spg_sql/parser.rs
1//! Recursive-descent parser with a Pratt (precedence-climbing) sub-parser for
2//! expressions.
3//!
4//! Precedence (lowest → highest binding):
5//! `OR` (1) `<` `AND` (2) `<` `NOT` unary (3) `<`
6//! comparisons `=` `<>` `<` `<=` `>` `>=` (4) `<`
7//! `+` `-` (5) `<` `*` `/` (6) `<` unary `-` (7) `<` parens / atom.
8//!
9//! This matches PG's behaviour for the operators we support — e.g. `NOT a = b`
10//! parses as `NOT (a = b)` and `-a * b` as `(-a) * b`.
11
12use alloc::boxed::Box;
13use alloc::format;
14use alloc::string::{String, ToString};
15use alloc::vec;
16use alloc::vec::Vec;
17use core::fmt;
18use core::mem;
19
20use crate::ast::{
21 AssignTarget, BinOp, CastTarget, Collation, ColumnDef, ColumnName, ColumnTypeName,
22 CreateFunctionStatement, CreateIndexStatement, CreatePublicationStatement,
23 CreateSubscriptionStatement, CreateTableStatement, CreateTriggerStatement, DiscardTarget, Expr,
24 ExtractField, FkAction, ForeignKeyConstraint, FrameBound, FrameExclusion, FrameKind,
25 FromClause, FromJoin, FunctionArg, FunctionArgMode, FunctionArgType, FunctionAttrs,
26 FunctionBody, FunctionParallel, FunctionReturn, FunctionVolatility, GrantObject, GrantPriv,
27 GrantStatement, IndexMethod, InsertStatement, IsolationLevel, JoinKind, Literal, MysqlIntWidth,
28 NullTreatment, OrderBy, Overriding, PlPgSqlBlock, PlPgSqlDeclare, PlPgSqlStmt,
29 PublicationScope, RaiseLevel, RangeKindAst, ReturnTarget, SelectItem, SelectStatement,
30 Statement, TableRef, TriggerEvent, TriggerForEach, TriggerTiming, UnOp, UnionKind, VecEncoding,
31 WindowFrame,
32};
33use crate::lexer::{self, LexError, Token};
34
35/// v7.38 — a `WINDOW w AS (…)` definition body:
36/// `(PARTITION BY exprs, ORDER BY (expr, desc, nulls_first), frame)`.
37type WindowDef = (
38 Vec<Expr>,
39 Vec<(Expr, bool, Option<bool>)>,
40 Option<WindowFrame>,
41);
42
43/// v7.14.0 — true when the leading keyword of a top-level
44/// statement is one of the dump-emitted DDL forms SPG accepts
45/// as a no-op (no behavioural effect on the single-schema /
46/// single-database model). These statements are consumed up to
47/// the next `;` / EOF and returned as `Statement::Empty`.
48/// v7.39 (read01 round 57) — wrap a parsed GRANT body in the right statement.
49fn finish_grant(grant: bool, g: GrantStatement) -> Statement {
50 if grant {
51 Statement::Grant(g)
52 } else {
53 Statement::Revoke(g)
54 }
55}
56
57fn is_dump_noise_statement(lc: &str) -> bool {
58 matches!(
59 lc,
60 // v7.39 (read01 round 50): "comment" moved OUT — COMMENT ON is now a
61 // real statement with a real store. v7.39 (read01 round 57): "grant" /
62 // "revoke" moved OUT — table privileges are now REAL (stored in
63 // `relacl`, enforced against the session role); a grant on any other
64 // object class still parses and no-ops so dumps restore.
65 // MySQL bulk-load brackets.
66 "unlock"
67 // MySQL OPTIMIZE / ANALYZE TABLE / CHECK TABLE
68 // diagnostics that pg_dump-style tools also emit
69 // post-restore.
70 | "optimize"
71 | "check"
72 | "use"
73 // PG psql backslash meta-commands that newer
74 // pg_dump versions emit unescaped (\restrict /
75 // \unrestrict). Real psql intercepts these; SPG's
76 // PG-wire sees them as raw text.
77 | "\\restrict"
78 | "\\unrestrict"
79 // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
80 // `DELIMITER ;` directives. Technically client-side
81 // (the `mysql` CLI uses them to set the statement
82 // terminator), not SQL — but mysqldump and stored-
83 // procedure scripts emit them inline. SPG's parser
84 // sees one statement at a time and doesn't care
85 // about the terminator, so consume DELIMITER lines
86 // as Empty.
87 | "delimiter"
88 // v7.37.17 (17.6 siblings) — additional PG maintenance /
89 // session-state statements pg_dump + application startup
90 // scripts emit. SPG has no matching session-state to
91 // discard (no prepared-plan cache surface, no temp
92 // sequences), no matching security-label / storage-
93 // option to apply, no separate CREATE/DROP CAST that
94 // affects execution.
95 // v7.37.17 (17.6 siblings) — PG role-cleanup statements
96 // pg_dump / pg_dumpall emit around DROP ROLE:
97 // REASSIGN OWNED BY <role> [, ...] TO <newrole>
98 // DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
99 // Both operate on the role's owned objects; SPG has no
100 // role-owner model, so accept-and-no-op.
101 // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
102 // + extension scripts use LOAD to preload shared
103 // libraries. SPG doesn't have a shared-library extension
104 // point today (extensions ship as first-class crates
105 // linked at build time); accept as a no-op.
106 | "load"
107 )
108}
109
110/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
111/// per `pg_get_keywords()`. SPG tokenizes these as named variants
112/// so the parser can dispatch on them in their owning contexts
113/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
114/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
115/// column / alias names — that's the PG contract for unreserved
116/// keywords (see PG docs Appendix C.1).
117///
118/// Before this generalisation, sentori migration 0001_init.sql
119/// `release TEXT NOT NULL` blew up the parser with "expected
120/// identifier, got Release", and the same gap stalked every
121/// SPG drop-in user whose schema had a column / alias named
122/// `release` / `index` / `tables` / `show` / `savepoint` /
123/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
124/// / `limit` / `partition`. PG accepts all of them as identifiers
125/// when unquoted, so SPG must too.
126///
127/// Returns the canonical lowercase identifier text when the token
128/// belongs to PG's unreserved class, `None` otherwise. Used by
129/// `expect_ident_like` (column / table / alias names) so the
130/// generalisation applies everywhere an identifier may appear,
131/// not just in the contexts these tokens were introduced for.
132fn unreserved_keyword_text(tok: &Token) -> Option<String> {
133 let s = match tok {
134 // PG keyword class: unreserved or col_name.
135 //
136 Token::Release => "release",
137 Token::Savepoint => "savepoint",
138 Token::Show => "show",
139 Token::Index => "index",
140 Token::Begin => "begin",
141 Token::Commit => "commit",
142 Token::Rollback => "rollback",
143 Token::Drop => "drop",
144 Token::Insert => "insert",
145 Token::Values => "values",
146 Token::Limit => "limit",
147 Token::Partition => "partition",
148 Token::Tables => "tables",
149 Token::Connection => "connection",
150 Token::Publication => "publication",
151 Token::Subscription => "subscription",
152 Token::Interval => "interval",
153 // `extract` is non-reserved in PG too (it's a function the
154 // parser dispatches via context — outside that context it's
155 // a plain identifier).
156 Token::Extract => "extract",
157 Token::Offset => "offset",
158 // `to` is reserved in PG (used in many "AS … TO …" forms), so
159 // it is NOT relaxed here. Same for `from`, `where`, `as`,
160 // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
161 // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
162 // `group`, `distinct`, `union`, `all`, `join`, `inner`,
163 // `left`, `cross`, `outer`, `default`, `is`, `between`,
164 // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
165 // (partial — keep partition as unreserved per modern PG).
166 _ => return None,
167 };
168 Some(s.to_string())
169}
170
171/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
172/// in CREATE INDEX. SPG's HNSW already routes by query operator;
173/// the opclass is accepted for `pg_dump` compatibility (mailrs
174/// migration follow-up G5).
175/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
176/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
177/// doesn't change index behaviour based on them.
178/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
179/// FROM-clause pipeline; the stored name tells the executor whether
180/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
181/// or unwraps to text (`*_each_text`).
182fn is_json_each_name(s: &str) -> bool {
183 s.eq_ignore_ascii_case("jsonb_each_text")
184 || s.eq_ignore_ascii_case("jsonb_each")
185 || s.eq_ignore_ascii_case("json_each_text")
186 || s.eq_ignore_ascii_case("json_each")
187}
188
189/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
190/// to positional order for the `make_*` family (the AST stays positional).
191/// Positional args fill slots left-to-right; a named arg goes to its registered
192/// slot; unfilled slots default to integer 0 (PG's optional make_interval
193/// fields — the make_date/time arity is still checked at eval time).
194fn reorder_named_args(
195 fname: &str,
196 args: Vec<Expr>,
197 names: &[Option<String>],
198) -> Result<Vec<Expr>, String> {
199 let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
200 "make_date" => &["year", "month", "day"],
201 "make_time" => &["hour", "min", "sec"],
202 "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
203 "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
204 other => {
205 return Err(alloc::format!(
206 "function {other}(...) does not support named arguments"
207 ));
208 }
209 };
210 let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
211 let mut next_positional = 0usize;
212 for (arg, name) in args.into_iter().zip(names.iter()) {
213 let idx = match name {
214 Some(n) => params
215 .iter()
216 .position(|p| p.eq_ignore_ascii_case(n))
217 .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
218 None => {
219 let i = next_positional;
220 next_positional += 1;
221 i
222 }
223 };
224 if idx >= slots.len() {
225 return Err(alloc::format!("too many arguments for {fname}(...)"));
226 }
227 if slots[idx].is_some() {
228 return Err(alloc::format!(
229 "argument \"{}\" specified more than once",
230 params[idx]
231 ));
232 }
233 slots[idx] = Some(arg);
234 }
235 Ok(slots
236 .into_iter()
237 .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
238 .collect())
239}
240
241/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
242/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
243/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
244/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
245/// scientific literal into PG's plain NUMERIC decimal form.
246#[derive(Debug)]
247pub enum SciExpanded {
248 /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
249 Expanded(String),
250 /// Exponent pushes the value outside PG's numeric format
251 /// (more than 131072 integer digits or 16383 fractional digits).
252 Overflow,
253 /// Not a `[±]digits[.digits]e[±]digits` literal at all.
254 NotScientific,
255}
256
257/// Expand scientific notation into a plain decimal string by moving the
258/// decimal point — no float round-trip, so the value stays exact. PG treats
259/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
260/// limits ("value overflows numeric format").
261pub fn expand_scientific_literal(s: &str) -> SciExpanded {
262 let s = s.trim();
263 let Some(epos) = s.find(['e', 'E']) else {
264 return SciExpanded::NotScientific;
265 };
266 let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
267 let Ok(exp) = exp_str.parse::<i64>() else {
268 return SciExpanded::NotScientific;
269 };
270 let (neg, mant) = match mant.strip_prefix('-') {
271 Some(r) => (true, r),
272 None => (false, mant.strip_prefix('+').unwrap_or(mant)),
273 };
274 let (int_part, frac_part) = match mant.split_once('.') {
275 Some((i, f)) => (i, f),
276 None => (mant, ""),
277 };
278 if (int_part.is_empty() && frac_part.is_empty())
279 || !int_part.bytes().all(|b| b.is_ascii_digit())
280 || !frac_part.bytes().all(|b| b.is_ascii_digit())
281 {
282 return SciExpanded::NotScientific;
283 }
284 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
285 digits.push_str(int_part);
286 digits.push_str(frac_part);
287 // Decimal point position within `digits` after applying the exponent.
288 let new_point = int_part.len() as i64 + exp;
289 // PG's numeric format: up to 131072 digits before the point, 16383 after.
290 if new_point > 131_072 {
291 return SciExpanded::Overflow;
292 }
293 if (digits.len() as i64 - new_point) > 16_383 {
294 return SciExpanded::Overflow;
295 }
296 let sign = if neg { "-" } else { "" };
297 let plain = if new_point <= 0 {
298 let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
299 out.push_str("0.");
300 for _ in 0..(-new_point) {
301 out.push('0');
302 }
303 out.push_str(&digits);
304 out
305 } else if (new_point as usize) >= digits.len() {
306 let mut out = digits;
307 for _ in 0..(new_point as usize - out.len()) {
308 out.push('0');
309 }
310 out
311 } else {
312 let mut out = String::with_capacity(digits.len() + 1);
313 out.push_str(&digits[..new_point as usize]);
314 out.push('.');
315 out.push_str(&digits[new_point as usize..]);
316 out
317 };
318 SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
319}
320
321/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
322/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
323/// are left-padded to an even count (`0x123` → byte string `01 23`, per
324/// MariaDB) and handed to the PG bytea input format (`\x…`).
325#[inline(never)]
326fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
327 let padded = if hex.len() % 2 == 1 {
328 alloc::format!("0{hex}")
329 } else {
330 hex.to_string()
331 };
332 Expr::Cast {
333 expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
334 "\\x{padded}"
335 )))),
336 target: CastTarget::Named("bytea".to_string()),
337 }
338}
339
340/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
341/// onto the bytea cast. The bits are read big-endian and left-padded to a
342/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
343#[inline(never)]
344fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
345 let pad = (8 - bits.len() % 8) % 8;
346 let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
347 let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
348 for nibble in padded.as_bytes().chunks(4) {
349 let mut v = 0u8;
350 for &b in nibble {
351 v = (v << 1) | (b - b'0');
352 }
353 hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
354 }
355 hex_literal_to_bytea_expr(&hex)
356}
357
358/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
359/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
360/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
361/// scale width (u8) falls back to double precision (recorded delta).
362/// Kept out of the parse_expr recursion frame — see the call site.
363#[inline(never)]
364fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
365 match parse_decimal_literal(&s) {
366 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
367 // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
368 // its exact value as a NumericBig.
369 None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
370 // v7.39 (read01 numeric.c) — expand the exponent form.
371 None => match expand_scientific_literal(&s) {
372 SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
373 Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
374 None if plain
375 .split_once('.')
376 .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
377 {
378 Ok(Literal::NumericBig(plain))
379 }
380 None => s
381 .parse::<f64>()
382 .map(Literal::Float)
383 .map_err(|_| format!("invalid numeric literal {s:?}")),
384 },
385 SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
386 SciExpanded::NotScientific => s
387 .parse::<f64>()
388 .map(Literal::Float)
389 .map_err(|_| format!("invalid numeric literal {s:?}")),
390 },
391 }
392}
393
394fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
395 let (int_part, frac_part) = match s.split_once('.') {
396 Some((i, f)) => (i, f),
397 None => (s, ""),
398 };
399 // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
400 // places fell out of the numeric path here, which is why
401 // `pg_typeof(1e-256)` answered double precision and a plain
402 // 256-place decimal aborted the query in the big-decimal converter.
403 if frac_part.len() > u16::MAX as usize {
404 return None;
405 }
406 let mut digits = String::with_capacity(int_part.len() + frac_part.len());
407 digits.push_str(int_part);
408 digits.push_str(frac_part);
409 let mantissa: i128 = digits.parse().ok()?;
410 #[allow(clippy::cast_possible_truncation)]
411 Some((mantissa, frac_part.len() as u16))
412}
413
414/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
415/// record-returning JSON functions that take a `AS alias(col type, …)`
416/// column-definition list in FROM position.
417fn is_json_to_record_name(s: &str) -> bool {
418 s.eq_ignore_ascii_case("jsonb_to_recordset")
419 || s.eq_ignore_ascii_case("jsonb_to_record")
420 // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
421 // column-definition list desugars identically (the record base
422 // argument only carries the type; a non-NULL base's field
423 // defaults are a recorded delta).
424 || s.eq_ignore_ascii_case("json_populate_record")
425 || s.eq_ignore_ascii_case("jsonb_populate_record")
426 || s.eq_ignore_ascii_case("json_populate_recordset")
427 || s.eq_ignore_ascii_case("jsonb_populate_recordset")
428 || s.eq_ignore_ascii_case("json_to_recordset")
429 || s.eq_ignore_ascii_case("json_to_record")
430}
431
432impl Parser {
433 /// Whether what follows an identifier ends an index key, which is how
434 /// an operator class is told from anything else in that position.
435 fn opclass_position_follows(next: Option<&Token>) -> bool {
436 match next {
437 // `ASC` / `DESC` have their own tokens; matching them as
438 // identifiers named "asc" / "desc" — which the first version of
439 // this did — never fires, and `(c text_pattern_ops DESC)` (which
440 // PG18.4 accepts, verified) went on failing to parse.
441 Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
442 Some(Token::Ident(w)) => {
443 w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
444 }
445 _ => false,
446 }
447 }
448}
449
450fn is_vector_opclass_name(name: &str) -> bool {
451 let lc = name.to_ascii_lowercase();
452 matches!(
453 lc.as_str(),
454 "vector_cosine_ops"
455 | "vector_l2_ops"
456 | "vector_ip_ops"
457 | "halfvec_cosine_ops"
458 | "halfvec_l2_ops"
459 | "halfvec_ip_ops"
460 | "sq8_cosine_ops"
461 | "sq8_l2_ops"
462 | "sq8_ip_ops"
463 // pg_trgm — trigram operator class. SPG's GIN index
464 // already uses tsvector tokens; trigram-style LIKE
465 // pattern matching still routes through a sequential
466 // scan, but the opclass name is accepted so PG schemas
467 // load.
468 | "gin_trgm_ops"
469 | "gist_trgm_ops"
470 // PG built-in btree opclasses occasionally appear in
471 // pg_dump output for column types with multiple
472 // sort orders (text_pattern_ops, varchar_pattern_ops,
473 // bpchar_pattern_ops).
474 | "text_pattern_ops"
475 | "varchar_pattern_ops"
476 | "bpchar_pattern_ops"
477 | "int4_ops"
478 | "int8_ops"
479 | "text_ops"
480 )
481}
482
483#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct ParseError {
485 pub message: String,
486 /// Index into the token stream where parsing tripped. Not a byte offset.
487 /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
488 /// field would grow every `Result<_, ParseError>` slot on the deeply
489 /// recursive parse stack and tip the nesting-budget frame cliff. PG's
490 /// 1-based char position is recovered on the cold error path by
491 /// [`syntax_error_position`], which re-tokenizes to map this token index.
492 pub token_pos: usize,
493}
494
495impl fmt::Display for ParseError {
496 /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
497 /// with `parse error at token #N: `, which PG has no equivalent of:
498 /// the message bodies are already PG's verbatim (`LIMIT must not be
499 /// negative`, `invalid input syntax for type bigint: "abc"`), and the
500 /// prefix was SPG's internal token index leaking into every one of
501 /// them. `token_pos` stays a field — the wire recovers PG's 1-based
502 /// character position from it for the ErrorResponse `P`.
503 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 f.write_str(&self.message)
505 }
506}
507
508impl From<LexError> for ParseError {
509 fn from(e: LexError) -> Self {
510 Self {
511 message: format!("lex: {e}"),
512 token_pos: 0,
513 }
514 }
515}
516
517/// v7.9.30 — parse a single expression (no trailing junk). Used by
518/// the engine to re-hydrate stored partial-index / unique-index
519/// predicates from their canonical Display form. The same Pratt
520/// parser the statement path uses; this entry point just skips the
521/// statement dispatch.
522pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
523 let (tokens, offsets) =
524 lexer::tokenize_with_offsets(input, false).map_err(|e| shape_lex_error(&e, input))?;
525 let mut p = Parser::new(tokens);
526 let expr = p
527 .parse_expr(0)
528 .and_then(|e| p.expect_eof().map(|()| e))
529 .map_err(|e| shape_syntax_error(e, input, &offsets))?;
530 Ok(expr)
531}
532
533/// Parse exactly one statement, swallow an optional trailing `;`, and require
534/// the token stream to end there. PG string semantics.
535pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
536 parse_statement_with(input, false)
537}
538
539/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
540/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
541/// The engine threads its session flag through here.
542pub fn parse_statement_with(input: &str, backslash_escapes: bool) -> Result<Statement, ParseError> {
543 let (tokens, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes)
544 .map_err(|e| shape_lex_error(&e, input))?;
545 // The same session flag names the dialect for both the lexer and
546 // the type mapping.
547 let mut p = Parser::new_with_dialect(tokens, backslash_escapes).with_source(input, &offsets);
548 let stmt = (|| {
549 let stmt = p.parse_one_statement()?;
550 if matches!(p.peek(), Token::Semicolon) {
551 p.advance();
552 }
553 p.expect_eof()?;
554 Ok(stmt)
555 })()
556 .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
557 Ok(stmt)
558}
559
560/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
561/// `syntax error at or near "<token>"` and `syntax error at end of input`
562/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
563/// prose — `expected identifier, got Eof`, `unexpected token From in
564/// expression`, `expected end of input, got Ident("with")` — which named
565/// internal token types and, in the Debug forms, leaked the parser's own
566/// enum into a message clients read.
567///
568/// Applied once on the way out, so every construction site is covered and
569/// the token named is the one the error itself points at. Messages whose
570/// bodies are already PG's verbatim (`LIMIT must not be negative`,
571/// `invalid input syntax for type bigint: "abc"`) are left alone — those
572/// are PG's own errors, not its syntax error.
573fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
574 if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
575 return e;
576 }
577 let message = match offending_lexeme(input, offsets, e.token_pos) {
578 Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
579 None => "syntax error at end of input".into(),
580 };
581 ParseError {
582 message,
583 token_pos: e.token_pos,
584 }
585}
586
587/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
588/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
589/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
590/// comment at or near "/* x"` — the quoted part runs from the opening
591/// delimiter to the end of the input. SPG reported its own internal
592/// shape instead (`unterminated string literal at byte 7`), which named
593/// a byte offset no client can use.
594fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
595 use lexer::LexErrorKind as K;
596 let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
597 let message = match &e.kind {
598 K::UnterminatedString => {
599 alloc::format!("unterminated quoted string at or near \"{from_here}\"")
600 }
601 K::UnterminatedQuotedIdent => {
602 alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
603 }
604 K::UnterminatedBlockComment => {
605 alloc::format!("unterminated /* comment at or near \"{from_here}\"")
606 }
607 // PG has no "unknown character" error of its own — the character
608 // is skipped and the parser reports the next token. SPG stops at
609 // the character itself and names it, which is the same shape.
610 K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
611 // The number-literal kinds already carry PG's `at or near` form.
612 other => alloc::format!(
613 "{}",
614 lexer::LexError {
615 kind: other.clone(),
616 pos: e.pos,
617 }
618 ),
619 };
620 ParseError {
621 message,
622 token_pos: 0,
623 }
624}
625
626/// The offending token exactly as it appears in the input, or `None` at
627/// end of input. PG echoes the source spelling — a lower-case `frm`
628/// reports as `frm`, not as a canonicalised keyword.
629fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
630 let start = *offsets.get(token_pos)?;
631 if start >= input.len() {
632 return None;
633 }
634 let end = offsets
635 .get(token_pos + 1)
636 .copied()
637 .unwrap_or(input.len())
638 .min(input.len());
639 let seg = input.get(start..end)?.trim();
640 if seg.is_empty() {
641 return None;
642 }
643 // A quoted literal / identifier keeps its inner spaces; anything else
644 // ends at the first whitespace (the segment runs to the NEXT token's
645 // start, which may swallow a comment).
646 if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
647 Some(seg)
648 } else {
649 seg.split_whitespace().next()
650 }
651}
652
653/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
654/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
655/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
656/// this re-tokenizes `input` on the cold error path to map the failing token
657/// index to its start byte, then to a character offset. `backslash_escapes`
658/// must match the parse that produced `token_pos` (it barely shifts offsets,
659/// but stay consistent). Returns `None` when the index has no offset or the
660/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
661#[must_use]
662pub fn syntax_error_position(
663 input: &str,
664 backslash_escapes: bool,
665 token_pos: usize,
666) -> Option<usize> {
667 let (_, offsets) = lexer::tokenize_with_offsets(input, backslash_escapes).ok()?;
668 let byte_off = *offsets.get(token_pos)?;
669 if byte_off > input.len() || !input.is_char_boundary(byte_off) {
670 return None;
671 }
672 Some(input[..byte_off].chars().count() + 1)
673}
674
675struct Parser {
676 tokens: Vec<Token>,
677 pos: usize,
678 /// v7.39 (round 274) — the session's dialect, carried by the same
679 /// signal that drives string-literal escaping: `SET sql_mode` (only
680 /// MySQL clients and mysqldump preambles emit it) turns it on,
681 /// `SET standard_conforming_strings` (every pg_dump preamble) turns
682 /// it off. Needed here because the two dialects disagree about what
683 /// `REAL` means — see the type mapping below.
684 mysql_dialect: bool,
685 /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
686 /// mutually recursive expr/select parsers. Bounded so a deeply
687 /// nested input returns a parse error instead of overflowing
688 /// the stack (embed hosts die on overflow — it is an abort,
689 /// not a catchable error).
690 nest_depth: usize,
691 /// TABLESAMPLE lowering channel: the table-ref parser pushes a
692 /// `random() < p/100` predicate here; the enclosing SELECT
693 /// drains the list after its WHERE parses and ANDs the
694 /// predicates in. parse_bare_select save/restores around its
695 /// FROM+WHERE so nested selects only drain their own.
696 pending_sample_preds: Vec<Expr>,
697 /// v7.39 (round 691) — collation lowering channel, the same shape as
698 /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
699 /// information, and `ast::OrderBy` is where this parser keeps ordering
700 /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
701 /// variant — puts a new arm on `eval_expr`, which this repo has
702 /// measured to overflow the debug stack. So while an ORDER BY KEY is
703 /// being parsed the postfix loop drops the name here instead of
704 /// refusing it, and the key's parser takes it.
705 ///
706 /// Only inside an ORDER BY key: everywhere else an unperformable
707 /// collation still errors, because accepting one at a COMPARISON and
708 /// ignoring it is the defect F36 exists to close.
709 in_order_by_key: bool,
710 order_key_collation: Option<String>,
711 /// POSITION(sub IN str) — while parsing the needle, the IN
712 /// keyword is the argument separator, not a membership test.
713 /// The postfix loop leaves IN unconsumed when this is set.
714 suppress_in_tail: bool,
715 /// Index of the token the last `advance()` returned — see
716 /// [`Parser::consumed_pos`].
717 last_consumed: usize,
718 /// v7.39 (round 506) — the statement's own text and the byte each token
719 /// starts at, so a MySQL projection item can report the SOURCE TEXT
720 /// MariaDB reports: `SELECT a + b` names its column `a + b`,
721 /// spacing and all. Only filled for a MySQL session — a PG one names
722 /// columns from the parsed shape and pays nothing for this.
723 src: Option<(String, Vec<usize>)>,
724}
725
726/// Max expr/select parser nesting (parens, subqueries, CASE, …).
727/// Real SQL nests a few dozen levels at the extreme. Each nesting level
728/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
729/// exists to turn a deep statement into a catchable parse ERROR: a stack
730/// overflow is an abort, and in the server it does not fail one query, it
731/// takes the process down and every other connection with it.
732///
733/// v7.39 (round 507) — measured, because the figure here used to be a
734/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
735/// in BOTH debug and release"), and the debug half of that is wrong by
736/// more than an order of magnitude:
737///
738/// * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
739/// this budget and errors. Verified against a live server for nested
740/// derived tables, parens, calls, CASE, IN-subqueries, scalar
741/// subqueries, NOT and unary minus — the server stayed up through all
742/// of them. This is the contract that matters, and it holds.
743/// * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
744/// LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
745/// and executing aborts around 8 inside a test thread. The budget is
746/// simply unreachable there, which is why a deep-nesting test has to
747/// ask for a large stack of its own — see `nesting_budget_errors_at`
748/// in the parser tests.
749/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
750/// one place.
751///
752/// There were two copies of this fact: a curated list, used for BARE
753/// names, and — in `try_peek_meta_qualified` — no list at all, which
754/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
755/// the engine to complain about a view it could not materialise. So
756/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
757/// had rows, `pg_catalog.pg_stat_activity` was an error.
758///
759/// PG puts `pg_catalog` at the implicit front of every search_path, so
760/// the two spellings name the same relation and must resolve the same
761/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
762/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
763/// meta_view_result path under their own names and must not be
764/// rewritten; a name that is neither reaches the ordinary resolver,
765/// which reports that the relation does not exist — PG's answer.
766const SYNTHESISED_PG_CATALOGS: &[&str] = &[
767 "pg_am",
768 "pg_attrdef",
769 "pg_attribute",
770 "pg_cast",
771 "pg_db_role_setting",
772 "pg_conversion",
773 "pg_default_acl",
774 "pg_shadow",
775 "pg_sequences",
776 "pg_range",
777 "pg_partitioned_table",
778 "pg_language",
779 "pg_group",
780 "pg_authid",
781 "pg_class",
782 "pg_collation",
783 "pg_constraint",
784 "pg_database",
785 "pg_depend",
786 // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
787 "pg_description",
788 "pg_enum",
789 "pg_extension",
790 // v7.39 (round 541) — pg_dump reads it for every relation of kind
791 // 'f'. SPG has no foreign tables, so it is empty, which is also
792 // what PG reports on a database that has none.
793 "pg_foreign_table",
794 // v7.39 (round 541) — the empty-by-truth family; see
795 // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
796 "pg_event_trigger",
797 "pg_file_settings",
798 "pg_foreign_data_wrapper",
799 "pg_foreign_server",
800 "pg_hba_file_rules",
801 "pg_ident_file_mappings",
802 "pg_init_privs",
803 "pg_parameter_acl",
804 "pg_prepared_xacts",
805 "pg_publication_namespace",
806 "pg_publication_rel",
807 "pg_publication_tables",
808 "pg_replication_origin",
809 "pg_replication_origin_status",
810 "pg_seclabel",
811 "pg_seclabels",
812 "pg_shdepend",
813 "pg_shdescription",
814 "pg_shmem_allocations",
815 "pg_shmem_allocations_numa",
816 "pg_shseclabel",
817 "pg_statistic_ext_data",
818 "pg_stats_ext",
819 "pg_stats_ext_exprs",
820 "pg_subscription_rel",
821 "pg_transform",
822 "pg_user_mapping",
823 "pg_user_mappings",
824 "pg_index",
825 "pg_indexes",
826 "pg_inherits",
827 // v7.39 (round 650) — the text-search catalogs SPG can fill
828 // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
829 // token types to dictionaries and SPG has no token-type model,
830 // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
831 "pg_ts_config",
832 "pg_ts_config_map",
833 "pg_ts_dict",
834 "pg_ts_parser",
835 "pg_ts_template",
836 "pg_matviews",
837 "pg_namespace",
838 // v7.39 (round 621)
839 "pg_operator",
840 "pg_policies",
841 "pg_policy",
842 "pg_proc",
843 "pg_publication",
844 "pg_replication_slots",
845 "pg_roles",
846 // v7.39 (round 143) — the rewrite-rule listing view.
847 // v7.39 (round 312) — and the rule catalogue itself, which
848 // `pg_get_ruledef(oid)` resolves against.
849 "pg_rewrite",
850 "pg_rules",
851 "pg_sequence",
852 "pg_settings",
853 "pg_stat_archiver",
854 "pg_stat_bgwriter",
855 "pg_stat_checkpointer",
856 "pg_stat_database",
857 "pg_stat_io",
858 "pg_stat_progress_analyze",
859 "pg_auth_members",
860 "pg_stat_progress_create_index",
861 "pg_stat_progress_vacuum",
862 "pg_stat_replication",
863 "pg_stat_slru",
864 "pg_stat_subscription_stats",
865 "pg_stat_user_functions",
866 "pg_stat_user_indexes",
867 "pg_stat_user_tables",
868 "pg_stat_wal",
869 "pg_prepared_statements",
870 "pg_largeobject",
871 "pg_largeobject_metadata",
872 "pg_statistic",
873 "pg_statistic_ext",
874 "pg_subscription",
875 "pg_tables",
876 "pg_tablespace",
877 // v7.39 (round 502) — the timezone catalogues. SPG resolved
878 // named zones correctly but could not list them, so a client
879 // populating a timezone picker got "relation does not exist".
880 "pg_timezone_abbrevs",
881 "pg_timezone_names",
882 "pg_trigger",
883 "pg_type",
884 "pg_user",
885 "pg_views",
886];
887
888const MAX_NEST_DEPTH: usize = 64;
889
890/// Stack accounting for the nesting budget, test-only.
891///
892/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
893/// that MOVES: a compiler upgrade grew the parser's debug frames and
894/// silently ate the margin until `nesting_budget_errors_cleanly` went
895/// from erroring cleanly to aborting on a stack overflow. A count
896/// cannot notice that on its own, so the budget is measured here and
897/// held to a ceiling.
898///
899/// The reading has to come from a helper whose OWN frame is the same at
900/// every call: debug slot placement does not follow source order, so a
901/// local's address inside the function under test is not that
902/// function's frame boundary. Two earlier probes were wrong that way —
903/// one read `&self.nest_depth`, which is the `Parser`'s address and
904/// never moves at all.
905#[cfg(test)]
906mod frame_meter {
907 extern crate std;
908 use std::cell::Cell;
909
910 // Per-THREAD, not global. `cargo test` runs tests in parallel and
911 // plenty of them parse nested expressions, so shared statics get
912 // stack addresses from several threads at once and the subtraction
913 // below turns into noise — it read 229,772 bytes per level that way,
914 // while passing when the test was run on its own.
915 std::thread_local! {
916 static AT_LO: Cell<usize> = const { Cell::new(0) };
917 static AT_HI: Cell<usize> = const { Cell::new(0) };
918 }
919
920 pub(super) const SAMPLE_LO: usize = 4;
921 pub(super) const SAMPLE_HI: usize = 24;
922
923 #[inline(never)]
924 pub(super) fn record(depth: usize) {
925 let anchor = 0u8;
926 let at = core::ptr::from_ref(&anchor) as usize;
927 if depth == SAMPLE_LO {
928 AT_LO.with(|c| c.set(at));
929 } else if depth == SAMPLE_HI {
930 AT_HI.with(|c| c.set(at));
931 }
932 }
933
934 /// Bytes of stack one nesting level costs, averaged over the span.
935 pub(super) fn bytes_per_level() -> usize {
936 let lo = AT_LO.with(Cell::get);
937 let hi = AT_HI.with(Cell::get);
938 assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
939 assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
940 (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
941 }
942
943 pub(super) fn reset() {
944 AT_LO.with(|c| c.set(0));
945 AT_HI.with(|c| c.set(0));
946 }
947}
948
949/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
950/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
951#[inline(never)]
952fn build_center_call(e: Expr) -> Expr {
953 Expr::FunctionCall {
954 name: alloc::string::String::from("center"),
955 args: alloc::vec![e],
956 }
957}
958
959/// Max consecutive binary operators at ONE precedence level
960/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
961/// parse time but evaluates and drops recursively — depth beyond
962/// this overflows 2 MiB worker stacks (debug eval frames run
963/// multiple KiB). `IN (…)` lists are flat and unaffected.
964const MAX_BINARY_CHAIN: usize = 256;
965
966/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
967/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
968/// it keeps its dedicated path (`parse_table_level_fk`).
969enum NamedTableConstraintKind {
970 Check,
971 Unique,
972 PrimaryKey,
973 Exclude,
974}
975
976impl Parser {
977 fn new(tokens: Vec<Token>) -> Self {
978 Self::new_with_dialect(tokens, false)
979 }
980
981 fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
982 Self {
983 tokens,
984 mysql_dialect,
985 in_order_by_key: false,
986 order_key_collation: None,
987 pos: 0,
988 nest_depth: 0,
989 pending_sample_preds: Vec::new(),
990 suppress_in_tail: false,
991 last_consumed: 0,
992 src: None,
993 }
994 }
995
996 /// Hand the parser the text it is parsing, for [`Parser::source_span`].
997 fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
998 if self.mysql_dialect {
999 self.src = Some((input.to_string(), offsets.to_vec()));
1000 }
1001 self
1002 }
1003
1004 /// The source text spanning tokens `start ..= end`, trimmed.
1005 ///
1006 /// The offsets are token STARTS, so the span runs to the start of the
1007 /// token after `end` and gives back the whitespace between them —
1008 /// trimming is what makes `a + b FROM t` end at `b`.
1009 fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1010 let (text, offsets) = self.src.as_ref()?;
1011 let from = *offsets.get(start)?;
1012 let to = *offsets.get(end + 1)?;
1013 text.get(from..to).map(str::trim_end)
1014 }
1015
1016 /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1017 /// nesting depth, erroring out cleanly past the budget.
1018 fn enter_nested(&mut self) -> Result<(), ParseError> {
1019 self.nest_depth += 1;
1020 #[cfg(test)]
1021 frame_meter::record(self.nest_depth);
1022 if self.nest_depth > MAX_NEST_DEPTH {
1023 self.nest_depth -= 1;
1024 return Err(self.err(alloc::format!(
1025 "statement nests deeper than {MAX_NEST_DEPTH} levels"
1026 )));
1027 }
1028 Ok(())
1029 }
1030
1031 fn peek(&self) -> &Token {
1032 // tokens always ends with Eof; pos is clamped in advance().
1033 &self.tokens[self.pos]
1034 }
1035
1036 fn advance(&mut self) -> Token {
1037 let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1038 self.last_consumed = self.pos;
1039 if self.pos + 1 < self.tokens.len() {
1040 self.pos += 1;
1041 }
1042 t
1043 }
1044
1045 /// v7.39 (round 340, V56) — the index of the token `advance()` just
1046 /// returned. It was computed as `pos - 1`, which is wrong at both
1047 /// ends: `advance()` parks on the final Eof rather than running off
1048 /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1049 /// input`), and after backtracking `pos` is no longer one past the
1050 /// token that failed. Recorded by `advance()` itself instead.
1051 const fn consumed_pos(&self) -> usize {
1052 self.last_consumed
1053 }
1054
1055 fn err(&self, message: String) -> ParseError {
1056 ParseError {
1057 message,
1058 token_pos: self.pos,
1059 }
1060 }
1061
1062 fn expect_eof(&self) -> Result<(), ParseError> {
1063 if matches!(self.peek(), Token::Eof) {
1064 Ok(())
1065 } else {
1066 Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1067 }
1068 }
1069
1070 /// v7.14.0 — swallow every token up to (but not including) the
1071 /// next semicolon / EOF. Used by the dump-noise dispatcher
1072 /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1073 /// etc. without modeling each grammar.
1074 /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1075 /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1076 /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1077 /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1078 /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1079 fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1080 let start = self.pos;
1081 self.advance(); // COMMENT
1082 if !matches!(self.peek(), Token::On) {
1083 self.pos = start;
1084 self.consume_until_statement_boundary();
1085 return Ok(Statement::Empty);
1086 }
1087 self.advance(); // ON
1088 let kind = match self.peek() {
1089 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1090 Token::Table => "table".into(),
1091 _ => {
1092 self.consume_until_statement_boundary();
1093 return Ok(Statement::Empty);
1094 }
1095 };
1096 if !matches!(
1097 kind.as_str(),
1098 "table"
1099 | "column"
1100 | "index"
1101 | "view"
1102 | "sequence"
1103 | "schema"
1104 | "type"
1105 | "database"
1106 | "function"
1107 ) {
1108 self.consume_until_statement_boundary();
1109 return Ok(Statement::Empty);
1110 }
1111 self.advance(); // the kind keyword
1112 // The object name. ⚠️ `expect_ident_like` strips a leading
1113 // `<schema>.` qualifier and returns only the trailing ident (SPG is
1114 // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1115 // `c`. Read the dotted parts from raw tokens instead, then let a
1116 // 3-part `schema.t.c` drop its leading schema like everywhere else.
1117 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1118 loop {
1119 match self.advance() {
1120 Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1121 other if unreserved_keyword_text(&other).is_some() => {
1122 parts.push(unreserved_keyword_text(&other).unwrap());
1123 }
1124 other => {
1125 return Err(ParseError {
1126 message: alloc::format!("expected identifier, got {other:?}"),
1127 token_pos: self.consumed_pos(),
1128 });
1129 }
1130 }
1131 if matches!(self.peek(), Token::Dot) {
1132 self.advance();
1133 } else {
1134 break;
1135 }
1136 }
1137 // COLUMN wants `table.column`; every other kind wants a bare name.
1138 let want = if kind == "column" { 2 } else { 1 };
1139 while parts.len() > want {
1140 parts.remove(0);
1141 }
1142 let name = parts.join(".");
1143 // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1144 // pg_dump writes the SIGNATURE, and the paren list was a syntax
1145 // error here — a dump carrying one function comment failed to
1146 // restore. The list is consumed (the comment store keys by name;
1147 // overload-precise comments are the function-predicate follow-up).
1148 if matches!(self.peek(), Token::LParen)
1149 && matches!(
1150 kind.as_str(),
1151 "function" | "procedure" | "aggregate" | "routine"
1152 )
1153 {
1154 let mut depth = 0usize;
1155 loop {
1156 match self.advance() {
1157 Token::LParen => depth += 1,
1158 Token::RParen => {
1159 depth -= 1;
1160 if depth == 0 {
1161 break;
1162 }
1163 }
1164 Token::Eof => {
1165 return Err(self.err(alloc::string::String::from(
1166 "unterminated argument list in COMMENT ON",
1167 )));
1168 }
1169 _ => {}
1170 }
1171 }
1172 }
1173 // `IS`
1174 if !matches!(self.peek(), Token::Is) {
1175 self.expect_keyword_ident("is")?;
1176 } else {
1177 self.advance();
1178 }
1179 let comment = match self.peek() {
1180 Token::Null => {
1181 self.advance();
1182 None
1183 }
1184 _ => Some(self.expect_string_literal()?),
1185 };
1186 Ok(Statement::CommentOn {
1187 kind,
1188 name,
1189 comment,
1190 })
1191 }
1192
1193 /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1194 /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1195 /// [CASCADE|RESTRICT]`.
1196 ///
1197 /// TABLE privileges are the real ones (stored, enforced, introspectable).
1198 /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1199 /// and the no-ON `GRANT role TO role` membership form — parses into
1200 /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1201 /// on them still restores.
1202 fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1203 self.advance(); // GRANT / REVOKE
1204 // REVOKE's optional `GRANT OPTION FOR` prefix.
1205 let mut grant_option = false;
1206 if !grant
1207 && self.peek_keyword_ident("grant")
1208 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1209 {
1210 self.advance(); // GRANT
1211 self.advance(); // OPTION
1212 self.expect_keyword_ident("for")?;
1213 grant_option = true;
1214 }
1215 // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1216 // words each with an optional COLUMN list.
1217 let mut privileges: Vec<GrantPriv> = Vec::new();
1218 if matches!(self.peek(), Token::All) {
1219 self.advance();
1220 if self.peek_keyword_ident("privileges") {
1221 self.advance();
1222 }
1223 // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1224 // column only.
1225 if matches!(self.peek(), Token::LParen) {
1226 let columns = self.parse_grant_column_list()?;
1227 privileges.push(GrantPriv {
1228 word: "ALL".into(),
1229 columns,
1230 });
1231 }
1232 } else {
1233 loop {
1234 // SELECT and INSERT lex as reserved tokens, so they never
1235 // reach `expect_ident_like` as plain idents; the rest
1236 // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1237 // MAINTAIN) are ordinary identifiers.
1238 let w = match self.peek() {
1239 Token::Select => {
1240 self.advance();
1241 "SELECT".to_string()
1242 }
1243 Token::Insert => {
1244 self.advance();
1245 "INSERT".to_string()
1246 }
1247 // v7.39 (read01 round 60) — CREATE is a privilege word on a
1248 // schema / database, and it lexes as a reserved token.
1249 Token::Create => {
1250 self.advance();
1251 "CREATE".to_string()
1252 }
1253 // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1254 // alice`) these "privilege words" are ROLE NAMES, and a
1255 // role name is case-sensitive. `priv_from_word` folds case
1256 // itself when they really are privileges.
1257 _ => self.expect_ident_like()?,
1258 };
1259 // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1260 // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1261 let columns = if matches!(self.peek(), Token::LParen) {
1262 self.parse_grant_column_list()?
1263 } else {
1264 Vec::new()
1265 };
1266 privileges.push(GrantPriv { word: w, columns });
1267 if matches!(self.peek(), Token::Comma) {
1268 self.advance();
1269 } else {
1270 break;
1271 }
1272 }
1273 }
1274 // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1275 // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1276 if !matches!(self.peek(), Token::On) {
1277 let roles: Vec<String> = core::mem::take(&mut privileges)
1278 .into_iter()
1279 .map(|p| p.word)
1280 .collect();
1281 let grantees = self.parse_grantee_list(grant)?;
1282 // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1283 // no admin-option layer: a member cannot re-grant).
1284 self.consume_until_statement_boundary();
1285 return Ok(finish_grant(
1286 grant,
1287 GrantStatement {
1288 privileges: Vec::new(),
1289 object: GrantObject::Roles(roles),
1290 grantees,
1291 grant_option,
1292 },
1293 ));
1294 }
1295 self.advance(); // ON
1296 // An optional object-class keyword. `TABLE` (or no keyword at all) is
1297 // the enforced case; anything else parses and no-ops.
1298 let mut class = "TABLE";
1299 match self.peek() {
1300 Token::Table => {
1301 self.advance();
1302 }
1303 Token::All => {
1304 // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1305 // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1306 // IN SCHEMA` stay no-ops and keep their own object class.
1307 self.advance(); // ALL
1308 let kind = match self.peek() {
1309 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1310 // TABLES has its own token (SHOW TABLES owns it).
1311 Token::Tables | Token::Table => "tables".to_string(),
1312 _ => String::new(),
1313 };
1314 if !kind.is_empty() {
1315 self.advance();
1316 }
1317 // `IN SCHEMA <name>`
1318 if matches!(self.peek(), Token::In) {
1319 self.advance();
1320 if self.peek_keyword_ident("schema") {
1321 self.advance();
1322 let _schema = self.expect_ident_like()?;
1323 }
1324 }
1325 if kind != "tables" {
1326 self.consume_until_statement_boundary();
1327 return Ok(finish_grant(
1328 grant,
1329 GrantStatement {
1330 privileges,
1331 object: GrantObject::Other("ALL … IN SCHEMA".into()),
1332 grantees: Vec::new(),
1333 grant_option,
1334 },
1335 ));
1336 }
1337 let grantees = self.parse_grantee_list(grant)?;
1338 self.consume_until_statement_boundary();
1339 return Ok(finish_grant(
1340 grant,
1341 GrantStatement {
1342 privileges,
1343 object: GrantObject::AllTablesInSchema,
1344 grantees,
1345 grant_option,
1346 },
1347 ));
1348 }
1349 Token::Ident(w) | Token::QuotedIdent(w) => {
1350 let lc = w.to_ascii_lowercase();
1351 // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1352 // real objects with real ACLs now.
1353 if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1354 self.advance();
1355 let mut names: Vec<String> = Vec::new();
1356 loop {
1357 let mut parts: Vec<String> = Vec::new();
1358 loop {
1359 parts.push(self.expect_ident_like()?);
1360 if matches!(self.peek(), Token::Dot) {
1361 self.advance();
1362 } else {
1363 break;
1364 }
1365 }
1366 names.push(parts.pop().expect("at least one part"));
1367 if matches!(self.peek(), Token::Comma) {
1368 self.advance();
1369 } else {
1370 break;
1371 }
1372 }
1373 let grantees = self.parse_grantee_list(grant)?;
1374 let mut grant_option = grant_option;
1375 if grant && self.peek_keyword_ident("with") {
1376 self.advance();
1377 self.expect_keyword_ident("grant")?;
1378 self.expect_keyword_ident("option")?;
1379 grant_option = true;
1380 }
1381 self.consume_until_statement_boundary();
1382 let object = match lc.as_str() {
1383 "sequence" => GrantObject::Sequences(names),
1384 "schema" => GrantObject::Schemas(names),
1385 _ => GrantObject::Databases(names),
1386 };
1387 return Ok(finish_grant(
1388 grant,
1389 GrantStatement {
1390 privileges,
1391 object,
1392 grantees,
1393 grant_option,
1394 },
1395 ));
1396 }
1397 // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1398 // keys functions by NAME, so the argument list parses and is
1399 // dropped (an overload set shares one ACL — recorded residual).
1400 if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1401 self.advance();
1402 let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1403 loop {
1404 let mut parts: Vec<String> = Vec::new();
1405 loop {
1406 parts.push(self.expect_ident_like()?);
1407 if matches!(self.peek(), Token::Dot) {
1408 self.advance();
1409 } else {
1410 break;
1411 }
1412 }
1413 let fname = parts.pop().expect("at least one part");
1414 // v7.39 (read01 round 62) — the signature picks the
1415 // overload, so it is captured.
1416 let sig = if matches!(self.peek(), Token::LParen) {
1417 Some(self.parse_function_signature_types()?)
1418 } else {
1419 None
1420 };
1421 names.push((fname, sig));
1422 if matches!(self.peek(), Token::Comma) {
1423 self.advance();
1424 } else {
1425 break;
1426 }
1427 }
1428 let grantees = self.parse_grantee_list(grant)?;
1429 self.consume_until_statement_boundary();
1430 return Ok(finish_grant(
1431 grant,
1432 GrantStatement {
1433 privileges,
1434 object: GrantObject::Functions(names),
1435 grantees,
1436 grant_option,
1437 },
1438 ));
1439 }
1440 if matches!(
1441 lc.as_str(),
1442 "type"
1443 | "domain"
1444 | "language"
1445 | "tablespace"
1446 | "large"
1447 | "foreign"
1448 | "parameter"
1449 ) {
1450 self.consume_until_statement_boundary();
1451 return Ok(finish_grant(
1452 grant,
1453 GrantStatement {
1454 privileges,
1455 object: GrantObject::Other(lc.to_ascii_uppercase()),
1456 grantees: Vec::new(),
1457 grant_option,
1458 },
1459 ));
1460 }
1461 class = "TABLE";
1462 }
1463 _ => {}
1464 }
1465 let _ = class;
1466 // The table list. Schema-qualified names drop their qualifier (SPG is
1467 // single-schema) — but read the dotted parts from raw tokens, since
1468 // `expect_ident_like` would silently swallow the leading part.
1469 let mut tables: Vec<String> = Vec::new();
1470 loop {
1471 let mut parts: Vec<String> = Vec::new();
1472 loop {
1473 parts.push(self.expect_ident_like()?);
1474 if matches!(self.peek(), Token::Dot) {
1475 self.advance();
1476 } else {
1477 break;
1478 }
1479 }
1480 tables.push(parts.pop().expect("at least one part"));
1481 if matches!(self.peek(), Token::Comma) {
1482 self.advance();
1483 } else {
1484 break;
1485 }
1486 }
1487 let grantees = self.parse_grantee_list(grant)?;
1488 if grant && self.peek_keyword_ident("with") {
1489 self.advance();
1490 self.expect_keyword_ident("grant")?;
1491 self.expect_keyword_ident("option")?;
1492 grant_option = true;
1493 }
1494 // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1495 // to cascade to (no re-granting), so both are accepted and ignored.
1496 if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1497 self.advance();
1498 }
1499 Ok(finish_grant(
1500 grant,
1501 GrantStatement {
1502 privileges,
1503 object: GrantObject::Tables(tables),
1504 grantees,
1505 grant_option,
1506 },
1507 ))
1508 }
1509
1510 /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1511 /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1512 /// words; the caller normalises them into a signature key.
1513 fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1514 self.advance(); // (
1515 let mut types: Vec<String> = Vec::new();
1516 if matches!(self.peek(), Token::RParen) {
1517 self.advance();
1518 return Ok(types);
1519 }
1520 loop {
1521 // Collect the words of one argument up to a comma / close paren.
1522 let mut words: Vec<String> = Vec::new();
1523 loop {
1524 match self.peek() {
1525 Token::Comma | Token::RParen | Token::Eof => break,
1526 _ => {}
1527 }
1528 let tok = self.advance();
1529 match tok {
1530 Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1531 other => {
1532 if let Some(w) = unreserved_keyword_text(&other) {
1533 words.push(w);
1534 }
1535 }
1536 }
1537 }
1538 // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1539 // themselves several words (`double precision`, `character
1540 // varying`, `timestamp with time zone`), so "two words means the
1541 // first is a parameter name" reads the type off `f(double
1542 // precision)` as `precision`. v7.39 (round 282): recognise the
1543 // multi-word spellings first — a leading word that STARTS one of
1544 // them is part of the type, not a name.
1545 let joined = words.join(" ");
1546 let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1547 joined
1548 } else if words.len() >= 2 {
1549 words[1..].join(" ")
1550 } else {
1551 words.first().cloned().unwrap_or_default()
1552 };
1553 types.push(ty);
1554 if matches!(self.peek(), Token::Comma) {
1555 self.advance();
1556 } else {
1557 break;
1558 }
1559 }
1560 if matches!(self.peek(), Token::RParen) {
1561 self.advance();
1562 }
1563 Ok(types)
1564 }
1565
1566 /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1567 fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1568 self.advance(); // (
1569 let mut cols = Vec::new();
1570 loop {
1571 cols.push(self.expect_ident_like()?);
1572 if matches!(self.peek(), Token::Comma) {
1573 self.advance();
1574 } else {
1575 break;
1576 }
1577 }
1578 if !matches!(self.peek(), Token::RParen) {
1579 return Err(self.err(alloc::format!(
1580 "expected ')' to close the column list, got {:?}",
1581 self.peek()
1582 )));
1583 }
1584 self.advance(); // )
1585 Ok(cols)
1586 }
1587
1588 /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1589 /// PUBLIC.
1590 fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1591 if grant {
1592 if matches!(self.peek(), Token::To) {
1593 self.advance();
1594 } else {
1595 self.expect_keyword_ident("to")?;
1596 }
1597 } else if matches!(self.peek(), Token::From) {
1598 self.advance();
1599 } else {
1600 self.expect_keyword_ident("from")?;
1601 }
1602 let mut grantees: Vec<String> = Vec::new();
1603 loop {
1604 // `GROUP name` is the legacy spelling of a plain role name.
1605 if self.peek_keyword_ident("group") {
1606 self.advance();
1607 }
1608 if self.peek_keyword_ident("public") {
1609 self.advance();
1610 grantees.push(String::new()); // PUBLIC
1611 } else {
1612 grantees.push(self.expect_ident_like()?);
1613 }
1614 if matches!(self.peek(), Token::Comma) {
1615 self.advance();
1616 } else {
1617 break;
1618 }
1619 }
1620 Ok(grantees)
1621 }
1622
1623 /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1624 /// The body keeps its `$N` placeholders; substitution happens at
1625 /// EXECUTE. The declared types are recorded for
1626 /// `pg_prepared_statements.parameter_types` but are not enforced —
1627 /// PG infers when the list is omitted, and SPG resolves the values
1628 /// at substitution time either way.
1629 fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1630 let start = self.pos;
1631 self.advance(); // PREPARE
1632 // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1633 // different statement that happens to share the keyword. PG
1634 // ships with `max_prepared_transactions = 0` and reports it
1635 // this way; SPG has no prepared-transaction registry, so the
1636 // same wording is the accurate answer rather than a dodge.
1637 // Round 277 turned this from a silent no-op into a confusing
1638 // "expected AS in PREPARE" parse error.
1639 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1640 self.advance();
1641 let gid = match self.advance() {
1642 Token::String(g) => g,
1643 other => {
1644 return Err(self.err(alloc::format!(
1645 "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1646 )));
1647 }
1648 };
1649 return Ok(Statement::PrepareTransaction(gid));
1650 }
1651 let name = self.expect_ident_like()?;
1652 let mut param_types = Vec::new();
1653 if matches!(self.peek(), Token::LParen) {
1654 self.advance();
1655 loop {
1656 let mut ty = self.expect_ident_like()?;
1657 // A parameterised type name (`numeric(10,2)`,
1658 // `varchar(20)`) keeps its argument list in the text.
1659 if matches!(self.peek(), Token::LParen) {
1660 let mut depth = 0usize;
1661 let mut buf = String::from("(");
1662 loop {
1663 match self.advance() {
1664 Token::LParen => {
1665 depth += 1;
1666 if depth > 1 {
1667 buf.push('(');
1668 }
1669 }
1670 Token::RParen => {
1671 depth -= 1;
1672 buf.push(')');
1673 if depth == 0 {
1674 break;
1675 }
1676 }
1677 Token::Comma => buf.push(','),
1678 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1679 Token::Eof => break,
1680 _ => {}
1681 }
1682 }
1683 ty.push_str(&buf);
1684 }
1685 // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1686 // position, same family as the parameter list above.
1687 let array_suffix = self.consume_array_suffix();
1688 ty.push_str(&array_suffix);
1689 param_types.push(ty);
1690 match self.peek() {
1691 Token::Comma => {
1692 self.advance();
1693 }
1694 Token::RParen => {
1695 self.advance();
1696 break;
1697 }
1698 other => {
1699 return Err(self.err(alloc::format!(
1700 "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1701 )));
1702 }
1703 }
1704 }
1705 }
1706 if !matches!(self.peek(), Token::As) {
1707 return Err(self.err(alloc::format!(
1708 "expected AS in PREPARE, got {:?}",
1709 self.peek()
1710 )));
1711 }
1712 self.advance();
1713 let body = self.parse_one_statement()?;
1714 // The Parser holds tokens, not the source text, so the
1715 // statement PG reports in `pg_prepared_statements.statement`
1716 // is rebuilt from the AST rather than sliced from the input.
1717 let _ = start;
1718 let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1719 if !param_types.is_empty() {
1720 source.push_str(" (");
1721 source.push_str(¶m_types.join(", "));
1722 source.push(')');
1723 }
1724 source.push_str(" AS ");
1725 source.push_str(&alloc::format!("{body}"));
1726 Ok(Statement::Prepare {
1727 name,
1728 param_types,
1729 body: alloc::boxed::Box::new(body),
1730 source,
1731 })
1732 }
1733
1734 /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1735 fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1736 self.advance(); // EXECUTE
1737 let name = self.expect_ident_like()?;
1738 let mut args = Vec::new();
1739 if matches!(self.peek(), Token::LParen) {
1740 self.advance();
1741 if matches!(self.peek(), Token::RParen) {
1742 self.advance();
1743 } else {
1744 loop {
1745 args.push(self.parse_expr(0)?);
1746 match self.advance() {
1747 Token::Comma => {}
1748 Token::RParen => break,
1749 other => {
1750 return Err(self.err(alloc::format!(
1751 "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1752 )));
1753 }
1754 }
1755 }
1756 }
1757 }
1758 Ok(Statement::Execute { name, args })
1759 }
1760
1761 /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1762 /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1763 /// procedure catalog yet, so this reports PG's not-found error
1764 /// (with its HINT) rather than pretending the call ran.
1765 /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1766 /// Bare `DISCARD` is a syntax error in PG; so it is here.
1767 fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1768 self.advance(); // DISCARD
1769 let target = match self.advance() {
1770 Token::All => DiscardTarget::All,
1771 Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1772 "all" => DiscardTarget::All,
1773 "plans" => DiscardTarget::Plans,
1774 "sequences" => DiscardTarget::Sequences,
1775 "temp" | "temporary" => DiscardTarget::Temp,
1776 other => {
1777 return Err(self.err(format!(
1778 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1779 )));
1780 }
1781 },
1782 other => {
1783 return Err(self.err(format!(
1784 "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1785 )));
1786 }
1787 };
1788 Ok(Statement::Discard(target))
1789 }
1790
1791 /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1792 /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1793 /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1794 /// aggressively the server interrupts, which SPG does not distinguish.
1795 /// Bare `KILL <id>` means CONNECTION.
1796 fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1797 self.advance(); // KILL
1798 let mut query_only = false;
1799 loop {
1800 // CONNECTION is a reserved keyword token (it also opens
1801 // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1802 // `Token::Connection` rather than a bare ident.
1803 if matches!(self.peek(), Token::Connection) {
1804 self.advance();
1805 break;
1806 }
1807 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1808 break;
1809 };
1810 match w.to_ascii_lowercase().as_str() {
1811 "hard" | "soft" => {
1812 self.advance();
1813 }
1814 "query" => {
1815 self.advance();
1816 query_only = true;
1817 break;
1818 }
1819 _ => break,
1820 }
1821 }
1822 let id = self.parse_expr(0)?;
1823 Ok(Statement::Kill {
1824 query_only,
1825 id: Box::new(id),
1826 })
1827 }
1828
1829 fn parse_call(&mut self) -> Result<Statement, ParseError> {
1830 self.advance(); // CALL
1831 let name = self.expect_ident_like()?;
1832 self.consume_until_statement_boundary();
1833 Ok(Statement::Call(name))
1834 }
1835
1836 fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1837 self.advance(); // DEALLOCATE
1838 // PG accepts an optional noise `PREPARE` keyword here.
1839 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1840 self.advance();
1841 }
1842 if matches!(self.peek(), Token::All) {
1843 self.advance();
1844 return Ok(Statement::Deallocate(None));
1845 }
1846 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1847 self.advance();
1848 return Ok(Statement::Deallocate(None));
1849 }
1850 let name = self.expect_ident_like()?;
1851 Ok(Statement::Deallocate(Some(name)))
1852 }
1853
1854 fn consume_until_statement_boundary(&mut self) {
1855 loop {
1856 match self.peek() {
1857 Token::Semicolon | Token::Eof => return,
1858 _ => self.advance(),
1859 };
1860 }
1861 }
1862
1863 /// v7.22 (round-13 T2) — consume to the statement boundary like
1864 /// `consume_until_statement_boundary`, but pick out the sequence
1865 /// name on the way: either `SEQUENCE NAME <ident>` (identity
1866 /// columns) or the first string literal (`nextval('<seq>')`).
1867 /// Schema qualifiers and `::regclass` casts are stripped.
1868 fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1869 let mut seq: Option<String> = None;
1870 let mut after_sequence_kw = false;
1871 let mut after_name_kw = false;
1872 loop {
1873 match self.peek().clone() {
1874 Token::Semicolon | Token::Eof => break,
1875 Token::Ident(s) | Token::QuotedIdent(s) => {
1876 if after_name_kw && seq.is_none() {
1877 self.advance();
1878 let mut name = s;
1879 // `SEQUENCE NAME public.groups_id_seq` — keep
1880 // the bare name, drop qualifiers.
1881 while matches!(self.peek(), Token::Dot) {
1882 self.advance();
1883 if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
1884 name = n;
1885 }
1886 }
1887 seq = Some(name);
1888 after_name_kw = false;
1889 continue;
1890 }
1891 if after_sequence_kw && s.eq_ignore_ascii_case("name") {
1892 after_name_kw = true;
1893 after_sequence_kw = false;
1894 } else {
1895 after_sequence_kw = s.eq_ignore_ascii_case("sequence");
1896 }
1897 self.advance();
1898 }
1899 Token::String(s) => {
1900 if seq.is_none() {
1901 // `nextval('public.groups_id_seq'::regclass)`
1902 let bare = s
1903 .rsplit_once('.')
1904 .map_or_else(|| s.clone(), |(_, b)| b.to_string());
1905 seq = Some(bare);
1906 }
1907 self.advance();
1908 }
1909 _ => {
1910 after_sequence_kw = false;
1911 after_name_kw = false;
1912 self.advance();
1913 }
1914 }
1915 }
1916 seq
1917 }
1918
1919 /// v7.39 (round 621) — is the next token the keyword `BY`?
1920 ///
1921 /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
1922 /// column, table and alias name — and SPG lexed it into a dedicated
1923 /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
1924 /// two-letter keywords the lexer knew, this was the only one PG leaves
1925 /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
1926 ///
1927 /// The token is gone; the three clauses that own the word — GROUP BY,
1928 /// ORDER BY, PARTITION BY — and the handful of other places that expect it
1929 /// ask this instead. Adding it to the unreserved-identifier table was not
1930 /// enough on its own: identifier positions that match the token shape
1931 /// directly (an index's column list, a table alias) never consult that
1932 /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
1933 /// Not lexing it as a keyword closes the whole class rather than the two
1934 /// positions that happened to be noticed.
1935 fn peek_is_by(&self) -> bool {
1936 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
1937 }
1938
1939 /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
1940 /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
1941 /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
1942 fn consume_drop_behaviour(&mut self) {
1943 if matches!(
1944 self.peek(),
1945 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
1946 ) {
1947 self.advance();
1948 }
1949 }
1950
1951 fn expect_ident_like(&mut self) -> Result<String, ParseError> {
1952 let first = match self.advance() {
1953 Token::Ident(s) | Token::QuotedIdent(s) => s,
1954 // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
1955 // per PG's `pg_get_keywords()` classification. SPG tokenizes
1956 // these as named variants for parsing leverage in the
1957 // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
1958 // `BEGIN`, etc.), but they MUST still be usable as table /
1959 // column / alias names in DDL+DML. Sentori migrations like
1960 // 0001_init.sql ship `release TEXT NOT NULL` in the events
1961 // table — the `events.release` column carries the release
1962 // identifier string. Pre-T4 this triggered "expected
1963 // identifier, got Release" and blocked every drop-in user
1964 // whose schema had a column / alias with one of these names.
1965 other if unreserved_keyword_text(&other).is_some() => {
1966 unreserved_keyword_text(&other).unwrap()
1967 }
1968 other => {
1969 return Err(ParseError {
1970 message: format!("expected identifier, got {other:?}"),
1971 token_pos: self.consumed_pos(),
1972 });
1973 }
1974 };
1975 // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
1976 // qualify every name with `public.` (and pg_catalog.* for
1977 // functions); SPG is single-schema so we discard the
1978 // prefix and return only the trailing ident. Same shape
1979 // also handles MySQL `db.tbl` cross-database refs (SPG
1980 // ignores the db part).
1981 if matches!(self.peek(), Token::Dot) {
1982 self.advance();
1983 match self.advance() {
1984 Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
1985 other if unreserved_keyword_text(&other).is_some() => {
1986 return Ok(unreserved_keyword_text(&other).unwrap());
1987 }
1988 other => {
1989 return Err(ParseError {
1990 message: format!("expected identifier after '{first}.', got {other:?}"),
1991 token_pos: self.consumed_pos(),
1992 });
1993 }
1994 }
1995 }
1996 Ok(first)
1997 }
1998
1999 #[allow(clippy::too_many_lines)]
2000 fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2001 // v7.14.0 — empty / comment-only / semicolon-only input
2002 // (after the lexer strips line + block + MySQL
2003 // conditional comments) lands as Statement::Empty.
2004 // pg_dump and mysqldump emit several wrappers that
2005 // collapse to nothing after stripping (`/*!40101 SET …
2006 // */;`, blank lines between statements); the engine
2007 // returns CommandOk no-op so the dump loads cleanly.
2008 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2009 return Ok(Statement::Empty);
2010 }
2011 // v7.14.0 — pg_dump / mysqldump "noise" statements:
2012 // catalog / metadata DDL that has no behavioural effect
2013 // on SPG's single-schema, single-database, single-user
2014 // model. Consume the whole statement up to the next
2015 // semicolon / EOF and return Empty. This is broader than
2016 // the per-keyword DROP / SET / COMMENT arms but lets the
2017 // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2018 // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2019 // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2020 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2021 let lc = s.to_ascii_lowercase();
2022 // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2023 if lc == "comment" {
2024 return self.parse_comment_on();
2025 }
2026 // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2027 if lc == "grant" || lc == "revoke" {
2028 return self.parse_grant_or_revoke(lc == "grant");
2029 }
2030 // v7.39 (round 277) — the SQL-level prepared-statement
2031 // surface is REAL now. It used to be accepted and dropped
2032 // on the theory that "real execution still happens via the
2033 // extended-query flow" — true only for a driver that uses
2034 // that flow; a plain SQL PREPARE / EXECUTE returned no
2035 // rows at all.
2036 if lc == "prepare" {
2037 return self.parse_prepare();
2038 }
2039 if lc == "execute" {
2040 return self.parse_execute();
2041 }
2042 if lc == "deallocate" {
2043 return self.parse_deallocate();
2044 }
2045 // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2046 // accepted and dropped, so an application's stored-procedure
2047 // invocation reported success and did nothing. SPG has no
2048 // procedure catalog, so every CALL names a procedure that
2049 // does not exist — which is exactly what PG says.
2050 if lc == "call" {
2051 return self.parse_call();
2052 }
2053 // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2054 // names one connection and acts on it.
2055 if lc == "kill" {
2056 return self.parse_kill();
2057 }
2058 if lc == "discard" {
2059 return self.parse_discard();
2060 }
2061 // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2062 // Still performs nothing; the roles are carried out so a name
2063 // that does not exist is refused, as PG18 refuses it.
2064 if lc == "reassign" {
2065 self.advance();
2066 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2067 self.advance();
2068 }
2069 if self.peek_is_by() {
2070 self.advance();
2071 }
2072 // Only the roles BEFORE the TO are the ones that must
2073 // exist — `TO` names the new owner, which PG checks as
2074 // well, so both lists are collected.
2075 let mut names = self.take_comma_separated_names();
2076 if matches!(self.peek(), Token::To) {
2077 self.advance();
2078 names.extend(self.take_comma_separated_names());
2079 }
2080 self.consume_until_statement_boundary();
2081 return Ok(Statement::ValidateOnly {
2082 kind: crate::ast::ValidateOnlyKind::RoleName,
2083 names,
2084 });
2085 }
2086 // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2087 // unconditionally with `no security label providers have been
2088 // loaded`, whatever object it names, because none is loaded.
2089 // SPG has none either; accepting it told the caller a label had
2090 // been applied when nothing anywhere records one.
2091 if lc == "security" {
2092 self.consume_until_statement_boundary();
2093 return Ok(Statement::ValidateOnly {
2094 kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2095 names: Vec::new(),
2096 });
2097 }
2098 if is_dump_noise_statement(&lc) {
2099 self.consume_until_statement_boundary();
2100 return Ok(Statement::Empty);
2101 }
2102 }
2103 match self.peek() {
2104 Token::Select => self.parse_select_stmt(),
2105 // v7.37.17 (17.6 siblings) — a statement opening with a
2106 // parenthesized query group: `(SELECT … UNION …)
2107 // INTERSECT …`. parse_bare_select's group arm consumes
2108 // the parens; the select parser handles the outer chain
2109 // and tail.
2110 Token::LParen
2111 if matches!(
2112 self.tokens.get(self.pos + 1),
2113 Some(Token::Select | Token::LParen | Token::Values)
2114 ) =>
2115 {
2116 self.parse_select_stmt()
2117 }
2118 // v7.37.17 (17.6 siblings) — top-level bare VALUES
2119 // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2120 // Lowers to the same UNION ALL chain the FROM-position
2121 // form uses, then reuses the shared SELECT tail.
2122 Token::Values => {
2123 self.advance(); // VALUES
2124 let mut head = self.parse_values_rows_body()?;
2125 self.parse_select_tail_into(&mut head)?;
2126 Ok(Statement::Select(head))
2127 }
2128 // SQL-standard `TABLE name` shorthand for
2129 // `SELECT * FROM name` — pg_dump never emits it, but
2130 // psql users and PG docs use it constantly. Set-op
2131 // chains and the ORDER BY/LIMIT tail compose like any
2132 // SELECT head.
2133 Token::Table
2134 if matches!(
2135 self.tokens.get(self.pos + 1),
2136 Some(Token::Ident(_) | Token::QuotedIdent(_))
2137 ) =>
2138 {
2139 let mut head = self.parse_table_shorthand()?;
2140 self.parse_setop_chain_into(&mut head)?;
2141 self.parse_select_tail_into(&mut head)?;
2142 Ok(Statement::Select(head))
2143 }
2144 // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2145 // body is a dollar-quoted plpgsql block (lexer already
2146 // collapsed `$$…$$` into a single Token::String).
2147 // v7.16.2 — mailrs round-10 A.2: parse the body as a
2148 // real PlPgSqlBlock so the engine can EXECUTE it at
2149 // top level instead of silently swallowing. Pre-
2150 // v7.16.2 the parser threw the body away and the
2151 // engine returned CommandOk for the entire DO; that
2152 // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2153 // $$` into a SEV-1 silent no-op (the IF + the rename
2154 // were both invisible — mailrs's migrate-042 didn't
2155 // actually run). Now the body parses + executes;
2156 // EmbeddedSql inside the block runs immediately
2157 // against the engine (not deferred — we're at top
2158 // level, not inside a trigger row-write loop).
2159 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2160 self.advance();
2161 let body_text = match self.advance() {
2162 Token::String(s) => s,
2163 other => {
2164 return Err(self.err(alloc::format!(
2165 "expected dollar-quoted body after DO, got {other:?}"
2166 )));
2167 }
2168 };
2169 // Optional `LANGUAGE <name>` trailer (idents only).
2170 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2171 self.advance();
2172 let _ = self.expect_ident_like()?;
2173 }
2174 // Parse the body — same shape CREATE FUNCTION
2175 // uses for trigger function bodies. If the body
2176 // doesn't parse cleanly we surface the error
2177 // (better than silent no-op).
2178 let block = parse_plpgsql_body(&body_text)?;
2179 Ok(Statement::DoBlock(block))
2180 }
2181 // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2182 // WITH isn't a reserved token in our lexer — comes through
2183 // as `Token::Ident("with")` (case-insensitive).
2184 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2185 self.advance();
2186 self.parse_with_cte_then_select()
2187 }
2188 // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2189 // an identifier — not a reserved keyword.
2190 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2191 self.advance();
2192 let mut analyze = false;
2193 let mut suggest = false;
2194 let mut costs_off = false;
2195 let mut buffers = false;
2196 let mut timing_off = false;
2197 let mut settings = false;
2198 let mut wal = false;
2199 let mut summary_off = false;
2200 let mut format = crate::ast::ExplainFormat::Text;
2201 // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2202 // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2203 // options are comma-separated. Booleans default to ON
2204 // when the value token is omitted (matches PG).
2205 if matches!(self.peek(), Token::LParen) {
2206 self.advance();
2207 loop {
2208 let opt = match self.peek().clone() {
2209 Token::Ident(s) | Token::QuotedIdent(s) => s,
2210 other => {
2211 return Err(self.err(format!(
2212 "expected option keyword inside EXPLAIN (…), got {other:?}"
2213 )));
2214 }
2215 };
2216 self.advance();
2217 if opt.eq_ignore_ascii_case("suggest") {
2218 suggest = true;
2219 // SUGGEST takes no explicit value today.
2220 } else if opt.eq_ignore_ascii_case("costs") {
2221 // PG syntax: `COSTS [ON | OFF]`. Default
2222 // when value omitted is ON, so plain
2223 // `COSTS` is a no-op. `COSTS OFF` flips.
2224 // `ON` lexes to `Token::On` (reserved
2225 // keyword in JOIN ... ON contexts); accept
2226 // it alongside the bare Ident form so the
2227 // grammar matches PG verbatim.
2228 let value = match self.peek().clone() {
2229 Token::On => {
2230 self.advance();
2231 true
2232 }
2233 Token::Ident(v) | Token::QuotedIdent(v)
2234 if v.eq_ignore_ascii_case("off") =>
2235 {
2236 self.advance();
2237 false
2238 }
2239 Token::Ident(v) | Token::QuotedIdent(v)
2240 if v.eq_ignore_ascii_case("true") =>
2241 {
2242 self.advance();
2243 true
2244 }
2245 _ => true,
2246 };
2247 costs_off = !value;
2248 } else if opt.eq_ignore_ascii_case("analyze")
2249 || opt.eq_ignore_ascii_case("analyse")
2250 {
2251 // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2252 // Same default-ON rule as ANALYZE keyword form.
2253 let value = match self.peek().clone() {
2254 Token::On => {
2255 self.advance();
2256 true
2257 }
2258 Token::Ident(v) | Token::QuotedIdent(v)
2259 if v.eq_ignore_ascii_case("off") =>
2260 {
2261 self.advance();
2262 false
2263 }
2264 Token::Ident(v) | Token::QuotedIdent(v)
2265 if v.eq_ignore_ascii_case("true") =>
2266 {
2267 self.advance();
2268 true
2269 }
2270 _ => true,
2271 };
2272 analyze = value;
2273 } else if opt.eq_ignore_ascii_case("buffers") {
2274 // v7.37.22 — `BUFFERS [ON|OFF]`.
2275 let value = match self.peek().clone() {
2276 Token::On => {
2277 self.advance();
2278 true
2279 }
2280 Token::Ident(v) | Token::QuotedIdent(v)
2281 if v.eq_ignore_ascii_case("off") =>
2282 {
2283 self.advance();
2284 false
2285 }
2286 Token::Ident(v) | Token::QuotedIdent(v)
2287 if v.eq_ignore_ascii_case("true") =>
2288 {
2289 self.advance();
2290 true
2291 }
2292 _ => true,
2293 };
2294 buffers = value;
2295 } else if opt.eq_ignore_ascii_case("timing") {
2296 // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2297 // the measured wall-clock annotation.
2298 let value = match self.peek().clone() {
2299 Token::On => {
2300 self.advance();
2301 true
2302 }
2303 Token::Ident(v) | Token::QuotedIdent(v)
2304 if v.eq_ignore_ascii_case("off") =>
2305 {
2306 self.advance();
2307 false
2308 }
2309 Token::Ident(v) | Token::QuotedIdent(v)
2310 if v.eq_ignore_ascii_case("true") =>
2311 {
2312 self.advance();
2313 true
2314 }
2315 _ => true,
2316 };
2317 timing_off = !value;
2318 } else if opt.eq_ignore_ascii_case("settings") {
2319 settings = true;
2320 } else if opt.eq_ignore_ascii_case("wal") {
2321 wal = true;
2322 } else if opt.eq_ignore_ascii_case("summary") {
2323 // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2324 // gates the trailing Planning/Execution Time
2325 // lines now (was accept-and-no-op).
2326 let value = match self.peek().clone() {
2327 Token::On => {
2328 self.advance();
2329 true
2330 }
2331 Token::Ident(v) | Token::QuotedIdent(v)
2332 if v.eq_ignore_ascii_case("off") =>
2333 {
2334 self.advance();
2335 false
2336 }
2337 Token::Ident(v) | Token::QuotedIdent(v)
2338 if v.eq_ignore_ascii_case("true") =>
2339 {
2340 self.advance();
2341 true
2342 }
2343 _ => true,
2344 };
2345 summary_off = !value;
2346 } else if opt.eq_ignore_ascii_case("verbose")
2347 || opt.eq_ignore_ascii_case("format")
2348 {
2349 // v7.37.22 — accept-but-no-op the remaining
2350 // PG options so EXPLAIN-using clients
2351 // (pgAdmin / DataGrip) don't see syntax
2352 // errors. FORMAT takes a value (text /
2353 // json / yaml / xml); skip the next token
2354 // if it's an ident.
2355 if opt.eq_ignore_ascii_case("format") {
2356 if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2357 {
2358 self.advance();
2359 format = match v.to_ascii_lowercase().as_str() {
2360 "text" => crate::ast::ExplainFormat::Text,
2361 "json" => crate::ast::ExplainFormat::Json,
2362 "xml" => crate::ast::ExplainFormat::Xml,
2363 "yaml" => crate::ast::ExplainFormat::Yaml,
2364 other => {
2365 return Err(self.err(format!(
2366 "EXPLAIN (FORMAT …): unknown format {other:?}; \
2367 supports text, json, xml, yaml"
2368 )));
2369 }
2370 };
2371 }
2372 } else {
2373 // VERBOSE / SUMMARY take optional ON/OFF;
2374 // consume if present.
2375 if matches!(self.peek(), Token::On) {
2376 self.advance();
2377 } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2378 self.peek().clone()
2379 && (v.eq_ignore_ascii_case("off")
2380 || v.eq_ignore_ascii_case("true"))
2381 {
2382 self.advance();
2383 let _ = v;
2384 }
2385 }
2386 } else {
2387 return Err(self.err(format!(
2388 "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2389 )));
2390 }
2391 if matches!(self.peek(), Token::Comma) {
2392 self.advance();
2393 continue;
2394 }
2395 break;
2396 }
2397 if !matches!(self.peek(), Token::RParen) {
2398 return Err(self.err(format!(
2399 "expected ')' after EXPLAIN options, got {:?}",
2400 self.peek()
2401 )));
2402 }
2403 self.advance();
2404 } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2405 && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2406 {
2407 self.advance();
2408 analyze = true;
2409 }
2410 // v7.39 (round 224) — the body may open with WITH (CTEs);
2411 // route through the same CTE-then-SELECT path the top-level
2412 // WITH statement uses. v7.39 (round 225) — DML bodies parse
2413 // too (PG explains INSERT / UPDATE / DELETE).
2414 let inner = match self.peek().clone() {
2415 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2416 self.advance();
2417 self.parse_with_cte_then_select()?
2418 }
2419 Token::Insert => self.parse_insert_stmt(false)?,
2420 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2421 self.advance();
2422 self.parse_update_after_keyword()?
2423 }
2424 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2425 self.advance();
2426 self.parse_delete_after_keyword()?
2427 }
2428 _ => self.parse_select_stmt()?,
2429 };
2430 if !matches!(
2431 inner,
2432 Statement::Select(_)
2433 | Statement::Insert(_)
2434 | Statement::Update(_)
2435 | Statement::Delete(_)
2436 ) {
2437 return Err(self.err(format!(
2438 "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2439 )));
2440 }
2441 Ok(Statement::Explain(crate::ast::ExplainStatement {
2442 analyze,
2443 inner: Box::new(inner),
2444 suggest,
2445 costs_off,
2446 buffers,
2447 timing_off,
2448 settings,
2449 wal,
2450 summary_off,
2451 format,
2452 }))
2453 }
2454 Token::Create => self.parse_create_stmt(),
2455 Token::Insert => self.parse_insert_stmt(false),
2456 // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2457 // spelling; route to the same handler. DESC is the
2458 // reserved ORDER BY token, so it gets its own arm.
2459 Token::Ident(s)
2460 if s.eq_ignore_ascii_case("describe")
2461 && matches!(
2462 self.tokens.get(self.pos + 1),
2463 Some(Token::Ident(_) | Token::QuotedIdent(_))
2464 ) =>
2465 {
2466 self.advance();
2467 let table = self.expect_ident_like()?;
2468 Ok(Statement::ShowColumns(table))
2469 }
2470 Token::Desc
2471 if matches!(
2472 self.tokens.get(self.pos + 1),
2473 Some(Token::Ident(_) | Token::QuotedIdent(_))
2474 ) =>
2475 {
2476 self.advance();
2477 let table = self.expect_ident_like()?;
2478 Ok(Statement::ShowColumns(table))
2479 }
2480 // `COPY table [(cols)] TO STDOUT` — the export half of
2481 // pg_dump's COPY pair (the FROM stdin half rides the
2482 // embed import path). Options need a format design and
2483 // error honestly.
2484 Token::Ident(s)
2485 if s.eq_ignore_ascii_case("copy")
2486 && matches!(
2487 self.tokens.get(self.pos + 1),
2488 Some(Token::Ident(_) | Token::QuotedIdent(_))
2489 ) =>
2490 {
2491 self.advance(); // COPY
2492 let table = self.expect_ident_like()?;
2493 let columns = if matches!(self.peek(), Token::LParen) {
2494 self.advance();
2495 let mut cols = alloc::vec![self.expect_ident_like()?];
2496 while matches!(self.peek(), Token::Comma) {
2497 self.advance();
2498 cols.push(self.expect_ident_like()?);
2499 }
2500 if !matches!(self.peek(), Token::RParen) {
2501 return Err(self.err(format!(
2502 "expected ')' after COPY column list, got {:?}",
2503 self.peek()
2504 )));
2505 }
2506 self.advance();
2507 Some(cols)
2508 } else {
2509 None
2510 };
2511 // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2512 // endpoint. (FROM STDIN still rides the wire/import path —
2513 // its data arrives out of band.)
2514 if matches!(self.peek(), Token::From)
2515 && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2516 {
2517 self.advance(); // FROM
2518 let Token::String(path) = self.advance() else {
2519 unreachable!()
2520 };
2521 let options = self.parse_copy_to_options()?;
2522 return Ok(Statement::CopyFromFile {
2523 table,
2524 columns,
2525 path,
2526 options,
2527 });
2528 }
2529 if !matches!(self.peek(), Token::To) {
2530 return Err(self.err(format!(
2531 "COPY: only TO STDOUT is supported here (FROM stdin \
2532 rides the import path); got {:?}",
2533 self.peek()
2534 )));
2535 }
2536 self.advance();
2537 if matches!(self.peek(), Token::String(_)) {
2538 let Token::String(path) = self.advance() else { unreachable!() };
2539 let options = self.parse_copy_to_options()?;
2540 return Ok(Statement::CopyToFile {
2541 table,
2542 columns,
2543 query: None,
2544 path,
2545 options,
2546 });
2547 }
2548 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2549 return Err(self.err(format!(
2550 "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2551 self.peek()
2552 )));
2553 }
2554 self.advance();
2555 let options = self.parse_copy_to_options()?;
2556 Ok(Statement::CopyTo {
2557 table,
2558 columns,
2559 query: None,
2560 options,
2561 })
2562 }
2563 // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2564 // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2565 // result set is streamed in COPY format (PG's query form).
2566 Token::Ident(s)
2567 if s.eq_ignore_ascii_case("copy")
2568 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2569 {
2570 self.advance(); // COPY
2571 self.advance(); // (
2572 let query = self.parse_select_stmt()?;
2573 if !matches!(self.peek(), Token::RParen) {
2574 return Err(self.err(format!(
2575 "expected ')' after COPY query, got {:?}",
2576 self.peek()
2577 )));
2578 }
2579 self.advance(); // )
2580 if !matches!(self.peek(), Token::To) {
2581 return Err(self.err(format!(
2582 "COPY (query): only TO STDOUT is supported, got {:?}",
2583 self.peek()
2584 )));
2585 }
2586 self.advance();
2587 if matches!(self.peek(), Token::String(_)) {
2588 let Token::String(path) = self.advance() else { unreachable!() };
2589 let options = self.parse_copy_to_options()?;
2590 return Ok(Statement::CopyToFile {
2591 table: String::new(),
2592 columns: None,
2593 query: Some(alloc::boxed::Box::new(query)),
2594 path,
2595 options,
2596 });
2597 }
2598 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2599 return Err(self.err(format!(
2600 "COPY (query): TO supports STDOUT only, got {:?}",
2601 self.peek()
2602 )));
2603 }
2604 self.advance();
2605 let options = self.parse_copy_to_options()?;
2606 Ok(Statement::CopyTo {
2607 table: String::new(),
2608 columns: None,
2609 query: Some(alloc::boxed::Box::new(query)),
2610 options,
2611 })
2612 }
2613 // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2614 // Shares the INSERT body; the replace flag lowers it
2615 // onto ON CONFLICT DO UPDATE with an empty assignment
2616 // list (engine: replace the whole row).
2617 Token::Ident(s)
2618 if s.eq_ignore_ascii_case("replace")
2619 && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2620 {
2621 self.parse_insert_stmt(true)
2622 }
2623 Token::Begin => {
2624 self.advance();
2625 // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2626 // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2627 // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2628 // is consumed first, then the trailing modes — including the
2629 // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2630 // WORK/TRANSACTION). The explicit level, when present, rides the
2631 // statement so `exec_begin` applies it for this transaction.
2632 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2633 {
2634 self.advance();
2635 }
2636 let iso = self.parse_isolation_level_clauses()?;
2637 Ok(Statement::Begin(iso))
2638 }
2639 // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2640 // for BEGIN. START is contextual in PG too; pattern-match
2641 // on the ident here. Iso clauses are parse-and-ignored,
2642 // same as BEGIN above.
2643 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2644 self.advance();
2645 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2646 {
2647 return Err(self.err(alloc::format!(
2648 "expected TRANSACTION after START, got {:?}",
2649 self.peek()
2650 )));
2651 }
2652 self.advance();
2653 let iso = self.parse_isolation_level_clauses()?;
2654 Ok(Statement::Begin(iso))
2655 }
2656 Token::Commit => {
2657 self.advance();
2658 Ok(Statement::Commit)
2659 }
2660 Token::Rollback => {
2661 self.advance();
2662 // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2663 // savepoint without ending the transaction. Bare
2664 // `ROLLBACK` drops the whole TX.
2665 if matches!(self.peek(), Token::To) {
2666 self.advance();
2667 if matches!(self.peek(), Token::Savepoint) {
2668 self.advance();
2669 }
2670 let name = self.expect_ident_like()?;
2671 Ok(Statement::RollbackToSavepoint(name))
2672 } else {
2673 Ok(Statement::Rollback)
2674 }
2675 }
2676 Token::Savepoint => {
2677 self.advance();
2678 let name = self.expect_ident_like()?;
2679 Ok(Statement::Savepoint(name))
2680 }
2681 Token::Release => {
2682 self.advance();
2683 // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2684 // is optional in standard SQL.
2685 if matches!(self.peek(), Token::Savepoint) {
2686 self.advance();
2687 }
2688 let name = self.expect_ident_like()?;
2689 Ok(Statement::ReleaseSavepoint(name))
2690 }
2691 Token::Show => {
2692 self.advance();
2693 // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2694 // v6.1.2 promoted TABLES to a reserved keyword (for
2695 // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2696 // arrives as `Token::Tables` rather than a bare ident.
2697 // USERS / COLUMNS remain bare idents.
2698 let target = match self.advance() {
2699 Token::Tables => "tables".to_string(),
2700 // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2701 // keyword token; recognise it as the SHOW CREATE
2702 // dispatch keyword too.
2703 Token::Create => "create".to_string(),
2704 // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2705 // keyword too; let SHOW INDEX FROM parse.
2706 Token::Index => "index".to_string(),
2707 // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2708 // reserved (used in aggregate function calls);
2709 // recognise it here so the parser dispatches
2710 // to ShowParameter("all") — the engine returns
2711 // the curated parameter inventory.
2712 Token::All => "all".to_string(),
2713 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2714 other => {
2715 return Err(self.err(format!(
2716 "expected SHOW target, got {other:?}"
2717 )));
2718 }
2719 };
2720 match target.as_str() {
2721 "tables" => Ok(Statement::ShowTables),
2722 "users" => Ok(Statement::ShowUsers),
2723 // v7.38 轴 4 — `SHOW transaction_isolation`
2724 // returns the currently-selected isolation level.
2725 "transaction_isolation" => Ok(Statement::ShowParameter(
2726 "transaction_isolation".to_string(),
2727 )),
2728 // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2729 // TABLE <t>` returns a 2-column row: (Table,
2730 // Create Table). mysqldump emits this for every
2731 // table at scrape time; without it the dump
2732 // round-trip stalls.
2733 // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2734 // FROM <t>` (also spelled `SHOW INDEX` and
2735 // `SHOW KEYS`). admin / mysqldump probes use
2736 // it to list per-table indexes.
2737 "indexes" | "index" | "keys" => {
2738 if !matches!(self.peek(), Token::From) {
2739 return Err(self.err(format!(
2740 "expected FROM after SHOW INDEXES, got {:?}",
2741 self.peek()
2742 )));
2743 }
2744 self.advance();
2745 let table = self.expect_ident_like()?;
2746 Ok(Statement::ShowIndexes(table))
2747 }
2748 // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2749 // `SHOW VARIABLES`. Both return a 2-column row
2750 // set listing server-side state; clients probe
2751 // them at connect time.
2752 "status" => Ok(Statement::ShowStatus),
2753 "variables" => Ok(Statement::ShowVariables),
2754 // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2755 "processlist" => Ok(Statement::ShowProcesslist),
2756 "create" => {
2757 // SHOW CREATE TABLE / VIEW / DATABASE — only
2758 // TABLE is supported in v7.17.
2759 let kind = match self.advance() {
2760 Token::Ident(s) | Token::QuotedIdent(s) => s,
2761 Token::Table => "table".to_string(),
2762 other => {
2763 return Err(self.err(format!(
2764 "expected TABLE after SHOW CREATE, got {other:?}"
2765 )));
2766 }
2767 };
2768 if !kind.eq_ignore_ascii_case("table") {
2769 return Err(self.err(format!(
2770 "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2771 )));
2772 }
2773 let name = self.expect_ident_like()?;
2774 Ok(Statement::ShowCreateTable(name))
2775 }
2776 // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2777 // (and `SHOW SCHEMAS` alias). The mysql client uses
2778 // it to populate the database selector at connect
2779 // time; without it `mysql -p` errors before the
2780 // first user query.
2781 "databases" | "schemas" => Ok(Statement::ShowDatabases),
2782 // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2783 // keyword on its own; it lands here as a bare
2784 // ident. Returning all publications + their
2785 // scope summary.
2786 "publications" => Ok(Statement::ShowPublications),
2787 // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2788 "subscriptions" => Ok(Statement::ShowSubscriptions),
2789 "columns" => {
2790 if !matches!(self.peek(), Token::From) {
2791 return Err(self.err(format!(
2792 "expected FROM after SHOW COLUMNS, got {:?}",
2793 self.peek()
2794 )));
2795 }
2796 self.advance();
2797 let table = self.expect_ident_like()?;
2798 Ok(Statement::ShowColumns(table))
2799 }
2800 // v7.38 轴 4 surface — `SHOW <param>` for any
2801 // remaining session / preset parameter name
2802 // (server_version, search_path, client_encoding,
2803 // …). The engine's ShowParameter handler does the
2804 // dispatch; unrecognised names error there with
2805 // a pointer to pg_settings, not at parse time —
2806 // so a driver that issues `SHOW spam_setting`
2807 // gets a clear runtime error instead of a
2808 // confusing "unknown SHOW target".
2809 other => {
2810 // v7.38 (read01 P3.20) — a custom namespaced GUC
2811 // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
2812 // consume the dotted tail so it round-trips with
2813 // `SET app.foo` / `current_setting('app.foo')`.
2814 let mut full = other.to_string();
2815 while matches!(self.peek(), Token::Dot) {
2816 self.advance();
2817 let seg = self.expect_ident_like()?;
2818 full.push('.');
2819 full.push_str(&seg.to_ascii_lowercase());
2820 }
2821 Ok(Statement::ShowParameter(full))
2822 }
2823 }
2824 }
2825 // v6.1.2: `DROP` is now a reserved keyword (it dispatches
2826 // to DROP USER and DROP PUBLICATION today; DROP TABLE /
2827 // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
2828 // arrived as a bare ident; tokenising it dedicatedly
2829 // keeps the dispatch tree small.
2830 Token::Drop => {
2831 self.advance();
2832 match self.peek() {
2833 // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
2834 // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
2835 // around DROP ROLE cleanup. SPG has no role-owner
2836 // model, so consume to boundary as a no-op.
2837 Token::Ident(s) | Token::QuotedIdent(s)
2838 if s.eq_ignore_ascii_case("owned") =>
2839 {
2840 // v7.39 (round 696) — still a no-op (SPG has no
2841 // role-owner model), but the ROLE is carried out so
2842 // the engine can refuse one that does not exist,
2843 // which is what PG18 does.
2844 self.advance();
2845 if self.peek_is_by() {
2846 self.advance();
2847 }
2848 let names = self.take_comma_separated_names();
2849 self.consume_until_statement_boundary();
2850 Ok(Statement::ValidateOnly {
2851 kind: crate::ast::ValidateOnlyKind::RoleName,
2852 names,
2853 })
2854 }
2855 // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
2856 // It drops only a TEMPORARY table, and name resolution
2857 // already prefers the session's own, so the keyword is
2858 // consumed and the ordinary DROP TABLE path runs.
2859 Token::Ident(s) | Token::QuotedIdent(s)
2860 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
2861 {
2862 self.advance();
2863 if !matches!(self.peek(), Token::Table) {
2864 return Err(self.err(alloc::format!(
2865 "expected TABLE after DROP TEMPORARY, got {:?}",
2866 self.peek()
2867 )));
2868 }
2869 self.parse_drop_table_after_keyword()
2870 }
2871 Token::Publication => {
2872 self.advance();
2873 // v7.39 (round 754, F31-B4) — the round-753
2874 // audit probe tripped over the missing
2875 // `IF EXISTS` here (syntax error).
2876 let if_exists = self.consume_if_exists();
2877 let name = self.expect_ident_or_string()?;
2878 Ok(Statement::DropPublication { name, if_exists })
2879 }
2880 Token::Subscription => {
2881 self.advance();
2882 let if_exists = self.consume_if_exists();
2883 let name = self.expect_ident_or_string()?;
2884 Ok(Statement::DropSubscription { name, if_exists })
2885 }
2886 Token::Ident(s) | Token::QuotedIdent(s)
2887 if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
2888 {
2889 self.advance();
2890 // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
2891 // login user IS a role in PG, and SPG's store holds
2892 // both. `IF EXISTS` is accepted on either spelling.
2893 let if_exists = self.consume_if_exists();
2894 let name = self.expect_ident_or_string()?;
2895 Ok(Statement::DropUser { name, if_exists })
2896 }
2897 // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
2898 // CREATE DATABASE has parsed since v7.14 and this did
2899 // not, so `DROP DATABASE IF EXISTS x` — what every
2900 // teardown script and pg_dumpall preamble opens with —
2901 // came back as a syntax error, which IF EXISTS cannot
2902 // soften. The name is carried so the engine can answer
2903 // the way PG does; PG never lets this succeed on a
2904 // single-database server, since the name is either
2905 // unknown ("database … does not exist", or a notice
2906 // under IF EXISTS) or the one you are connected to
2907 // ("cannot drop the currently open database").
2908 Token::Ident(s) | Token::QuotedIdent(s)
2909 if s.eq_ignore_ascii_case("database") =>
2910 {
2911 self.advance();
2912 let if_exists = self.consume_if_exists();
2913 let name = self.expect_ident_or_string()?;
2914 self.consume_until_statement_boundary();
2915 Ok(Statement::DropDatabase { name, if_exists })
2916 }
2917 // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
2918 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
2919 self.advance();
2920 let if_exists = self.consume_if_exists();
2921 let name = self.expect_ident_like()?;
2922 // ON <table>
2923 if !matches!(self.peek(), Token::On) {
2924 return Err(self.err(alloc::format!(
2925 "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
2926 self.peek()
2927 )));
2928 }
2929 self.advance();
2930 let table = self.expect_ident_like()?;
2931 Ok(Statement::DropTrigger {
2932 name,
2933 table,
2934 if_exists,
2935 })
2936 }
2937 // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
2938 // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
2939 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
2940 self.advance();
2941 let if_exists = self.consume_if_exists();
2942 let name = self.expect_ident_like()?;
2943 if !matches!(self.peek(), Token::On) {
2944 return Err(self.err(alloc::format!(
2945 "expected ON <table> after DROP RULE {name:?}, got {:?}",
2946 self.peek()
2947 )));
2948 }
2949 self.advance();
2950 let table = self.expect_ident_like()?;
2951 // Optional CASCADE / RESTRICT — accepted, no effect.
2952 self.consume_until_statement_boundary();
2953 Ok(Statement::DropRule {
2954 name,
2955 table,
2956 if_exists,
2957 })
2958 }
2959 // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
2960 // v7.12.4 ignores any optional arg-list (signature-
2961 // based overload disambiguation lands in v7.12.5+).
2962 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
2963 self.advance();
2964 let if_exists = self.consume_if_exists();
2965 let name = self.expect_ident_like()?;
2966 // v7.39 (read01 round 62) — the argument list identifies
2967 // WHICH overload to drop, so it is captured, not
2968 // discarded. `DROP FUNCTION f` (no list) is legal when
2969 // the name is unambiguous; the engine enforces that.
2970 let args = if matches!(self.peek(), Token::LParen) {
2971 Some(self.parse_function_signature_types()?)
2972 } else {
2973 None
2974 };
2975 // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
2976 // trailer, which `DROP TABLE` and `DROP INDEX` have
2977 // accepted since v7.14 and this one refused outright.
2978 // pg_dump writes it, so refusing was a parse error in
2979 // the middle of a restore. SPG drops the function
2980 // either way — it tracks no dependents to cascade to —
2981 // which is the same reading the other two give it.
2982 self.consume_drop_behaviour();
2983 Ok(Statement::DropFunction {
2984 name,
2985 args,
2986 if_exists,
2987 })
2988 }
2989 // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
2990 // [CASCADE|RESTRICT]. pg_dump and mysqldump both
2991 // emit DROP TABLE IF EXISTS at the head of every
2992 // CREATE TABLE block so re-importing a dump
2993 // overwrites prior state. SPG accepts and removes
2994 // matching tables; CASCADE/RESTRICT trailers
2995 // accepted silently.
2996 Token::Table => self.parse_drop_table_after_keyword(),
2997 // v7.14.0 — DROP INDEX [IF EXISTS] name
2998 // [CASCADE|RESTRICT]. PG / mysqldump emit this
2999 // for partial-index renames and pgvector
3000 // migrations. SPG removes the matching index;
3001 // IF EXISTS makes the drop idempotent.
3002 Token::Index => {
3003 self.advance();
3004 let if_exists = self.consume_if_exists();
3005 let name = self.expect_ident_like()?;
3006 if matches!(
3007 self.peek(),
3008 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3009 || s.eq_ignore_ascii_case("restrict")
3010 ) {
3011 self.advance();
3012 }
3013 Ok(Statement::DropIndex { name, if_exists })
3014 }
3015 // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3016 // [CASCADE|RESTRICT]. SPG is single-database;
3017 // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3018 // name [, name…] [CASCADE | RESTRICT]. Real
3019 // unregister (was silent no-op pre-v7.17).
3020 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3021 self.advance();
3022 let if_exists = self.consume_if_exists();
3023 let mut names = vec![self.expect_ident_like()?];
3024 while matches!(self.peek(), Token::Comma) {
3025 self.advance();
3026 names.push(self.expect_ident_like()?);
3027 }
3028 if matches!(
3029 self.peek(),
3030 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3031 || s.eq_ignore_ascii_case("restrict")
3032 ) {
3033 self.advance();
3034 }
3035 Ok(Statement::DropSchema { names, if_exists })
3036 }
3037 // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3038 // name [, name…] [CASCADE|RESTRICT].
3039 Token::Ident(s) | Token::QuotedIdent(s)
3040 if s.eq_ignore_ascii_case("type") =>
3041 {
3042 self.advance();
3043 let if_exists = self.consume_if_exists();
3044 let mut names = vec![self.expect_ident_like()?];
3045 while matches!(self.peek(), Token::Comma) {
3046 self.advance();
3047 names.push(self.expect_ident_like()?);
3048 }
3049 if matches!(
3050 self.peek(),
3051 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3052 || s.eq_ignore_ascii_case("restrict")
3053 ) {
3054 self.advance();
3055 }
3056 Ok(Statement::DropType { names, if_exists })
3057 }
3058 // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3059 // name [, name…] [CASCADE|RESTRICT].
3060 Token::Ident(s) | Token::QuotedIdent(s)
3061 if s.eq_ignore_ascii_case("domain") =>
3062 {
3063 self.advance();
3064 let if_exists = self.consume_if_exists();
3065 let mut names = vec![self.expect_ident_like()?];
3066 while matches!(self.peek(), Token::Comma) {
3067 self.advance();
3068 names.push(self.expect_ident_like()?);
3069 }
3070 if matches!(
3071 self.peek(),
3072 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3073 || s.eq_ignore_ascii_case("restrict")
3074 ) {
3075 self.advance();
3076 }
3077 Ok(Statement::DropDomain { names, if_exists })
3078 }
3079 // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3080 // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3081 Token::Ident(s) | Token::QuotedIdent(s)
3082 if s.eq_ignore_ascii_case("materialized") =>
3083 {
3084 self.advance();
3085 let nxt = self.peek().clone();
3086 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3087 {
3088 return Err(self.err(alloc::format!(
3089 "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3090 )));
3091 }
3092 self.advance();
3093 let if_exists = self.consume_if_exists();
3094 let mut names = vec![self.expect_ident_like()?];
3095 while matches!(self.peek(), Token::Comma) {
3096 self.advance();
3097 names.push(self.expect_ident_like()?);
3098 }
3099 if matches!(
3100 self.peek(),
3101 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3102 || s.eq_ignore_ascii_case("restrict")
3103 ) {
3104 self.advance();
3105 }
3106 Ok(Statement::DropMaterializedView { names, if_exists })
3107 }
3108 // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3109 // name [, name…] [CASCADE|RESTRICT].
3110 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3111 self.advance();
3112 let if_exists = self.consume_if_exists();
3113 let mut names = vec![self.expect_ident_like()?];
3114 while matches!(self.peek(), Token::Comma) {
3115 self.advance();
3116 names.push(self.expect_ident_like()?);
3117 }
3118 if matches!(
3119 self.peek(),
3120 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3121 || s.eq_ignore_ascii_case("restrict")
3122 ) {
3123 self.advance();
3124 }
3125 Ok(Statement::DropView { names, if_exists })
3126 }
3127 // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3128 // [CASCADE|RESTRICT]. Real removal from catalog
3129 // (was a silent no-op pre-v7.17).
3130 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3131 self.advance();
3132 let if_exists = self.consume_if_exists();
3133 let mut names = vec![self.expect_ident_like()?];
3134 while matches!(self.peek(), Token::Comma) {
3135 self.advance();
3136 names.push(self.expect_ident_like()?);
3137 }
3138 if matches!(
3139 self.peek(),
3140 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3141 || s.eq_ignore_ascii_case("restrict")
3142 ) {
3143 self.advance();
3144 }
3145 Ok(Statement::DropSequence { names, if_exists })
3146 }
3147 // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3148 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3149 self.advance();
3150 self.parse_drop_policy_after_keyword()
3151 }
3152 // v7.37.17 (17.6 siblings) — DROP <target> for
3153 // targets SPG doesn't natively track. pg_dump
3154 // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3155 // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3156 // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3157 // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3158 // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3159 // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3160 // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3161 // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3162 // etc. — accept + Empty-return so pg_dump tails
3163 // load through. Materialized-view drop dispatches
3164 // to the existing DropTable path when the token
3165 // is Materialized-View-shaped (elsewhere in
3166 // this parser).
3167 Token::Ident(s) | Token::QuotedIdent(s)
3168 if s.eq_ignore_ascii_case("text")
3169 // The DROP dispatch matches on PEEK — `text` is
3170 // not yet consumed, so SEARCH/CONFIGURATION sit
3171 // at pos+1/pos+2 (the round-695 trap's mirror).
3172 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3173 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3174 {
3175 // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3176 // validates the name; DICTIONARY / PARSER / TEMPLATE
3177 // stay in the noise arm below.
3178 self.advance(); // TEXT
3179 self.advance(); // SEARCH
3180 self.advance(); // CONFIGURATION
3181 let if_exists = self.consume_if_exists();
3182 let names = self.take_comma_separated_names();
3183 self.consume_until_statement_boundary();
3184 if if_exists {
3185 return Ok(Statement::Empty);
3186 }
3187 Ok(Statement::ValidateOnly {
3188 kind: crate::ast::ValidateOnlyKind::TsConfigName,
3189 names,
3190 })
3191 }
3192 Token::Ident(s) | Token::QuotedIdent(s)
3193 if matches!(
3194 s.to_ascii_lowercase().as_str(),
3195 "type"
3196 | "domain"
3197 | "operator"
3198 | "cast"
3199 // `text` = TEXT SEARCH DICTIONARY / PARSER /
3200 // TEMPLATE (CONFIGURATION intercepted above).
3201 | "text"
3202 | "materialized"
3203 | "large"
3204 | "role"
3205 | "access"
3206 | "procedure"
3207 | "routine"
3208 ) =>
3209 {
3210 self.consume_until_statement_boundary();
3211 Ok(Statement::Empty)
3212 }
3213 // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3214 // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3215 // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3216 // foreign-data warning family (round 706) so a
3217 // CREATE→DROP sequence in a dump stays consistent.
3218 Token::Ident(s) | Token::QuotedIdent(s)
3219 if s.eq_ignore_ascii_case("server")
3220 || s.eq_ignore_ascii_case("foreign") =>
3221 {
3222 self.advance();
3223 self.consume_until_statement_boundary();
3224 Ok(Statement::ValidateOnly {
3225 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3226 names: Vec::new(),
3227 })
3228 }
3229 Token::Ident(s) | Token::QuotedIdent(s)
3230 if s.eq_ignore_ascii_case("collation")
3231 || s.eq_ignore_ascii_case("tablespace") =>
3232 {
3233 let kind = if s.eq_ignore_ascii_case("collation") {
3234 crate::ast::ValidateOnlyKind::CollationName
3235 } else {
3236 crate::ast::ValidateOnlyKind::TablespaceName
3237 };
3238 self.advance();
3239 let if_exists = self.consume_if_exists();
3240 let names = self.take_comma_separated_names();
3241 self.consume_until_statement_boundary();
3242 if if_exists {
3243 return Ok(Statement::Empty);
3244 }
3245 Ok(Statement::ValidateOnly { kind, names })
3246 }
3247 Token::Ident(s) | Token::QuotedIdent(s)
3248 if s.eq_ignore_ascii_case("event") =>
3249 {
3250 self.advance();
3251 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3252 {
3253 self.advance();
3254 }
3255 let if_exists = self.consume_if_exists();
3256 let names = self.take_comma_separated_names();
3257 self.consume_until_statement_boundary();
3258 if if_exists {
3259 return Ok(Statement::Empty);
3260 }
3261 Ok(Statement::ValidateOnly {
3262 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3263 names,
3264 })
3265 }
3266 // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3267 // leave the noise list; see the ValidateOnly kinds.
3268 Token::Ident(s) | Token::QuotedIdent(s)
3269 if s.eq_ignore_ascii_case("conversion")
3270 || s.eq_ignore_ascii_case("language")
3271 // `DROP PROCEDURAL LANGUAGE` puts the modifier
3272 // FIRST — the first draft looked for it after.
3273 || s.eq_ignore_ascii_case("procedural") =>
3274 {
3275 let kind = if s.eq_ignore_ascii_case("conversion") {
3276 crate::ast::ValidateOnlyKind::ConversionName
3277 } else {
3278 crate::ast::ValidateOnlyKind::LanguageName
3279 };
3280 self.advance();
3281 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3282 {
3283 self.advance();
3284 }
3285 let if_exists = self.consume_if_exists();
3286 let names = self.take_comma_separated_names();
3287 self.consume_until_statement_boundary();
3288 if if_exists {
3289 return Ok(Statement::Empty);
3290 }
3291 Ok(Statement::ValidateOnly { kind, names })
3292 }
3293 // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3294 // name(argtypes)[, …]`. Parsed for real so the engine
3295 // can answer as PG does; see Statement::DropAggregate.
3296 Token::Ident(s) | Token::QuotedIdent(s)
3297 if s.eq_ignore_ascii_case("aggregate") =>
3298 {
3299 self.advance();
3300 let if_exists = self.consume_if_exists();
3301 let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3302 loop {
3303 let name = self.expect_ident_like()?;
3304 if !matches!(self.peek(), Token::LParen) {
3305 return Err(self.err(alloc::format!(
3306 "expected argument list after DROP AGGREGATE {name}"
3307 )));
3308 }
3309 self.advance();
3310 let mut args: Vec<String> = Vec::new();
3311 let mut star = false;
3312 loop {
3313 match self.peek().clone() {
3314 Token::RParen => {
3315 self.advance();
3316 break;
3317 }
3318 Token::Star => {
3319 self.advance();
3320 star = true;
3321 }
3322 Token::Comma => {
3323 self.advance();
3324 }
3325 _ => {
3326 // A type name may be multi-token
3327 // (`double precision`); glue idents
3328 // until , or ).
3329 let mut t = self.expect_ident_like()?;
3330 while let Token::Ident(nx) = self.peek() {
3331 let nx = nx.clone();
3332 self.advance();
3333 t.push(' ');
3334 t.push_str(&nx);
3335 }
3336 args.push(t);
3337 }
3338 }
3339 }
3340 items.push((name, if star { None } else { Some(args) }));
3341 if matches!(self.peek(), Token::Comma) {
3342 self.advance();
3343 } else {
3344 break;
3345 }
3346 }
3347 self.consume_until_statement_boundary();
3348 Ok(Statement::DropAggregate { if_exists, items })
3349 }
3350 // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3351 // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3352 // installed; `IF EXISTS` is the spelling that says do
3353 // not, and it keeps the no-op.
3354 Token::Ident(s) | Token::QuotedIdent(s)
3355 if s.eq_ignore_ascii_case("extension") =>
3356 {
3357 self.advance();
3358 let if_exists = self.consume_if_exists();
3359 let names = self.take_comma_separated_names();
3360 self.consume_until_statement_boundary();
3361 if if_exists {
3362 return Ok(Statement::Empty);
3363 }
3364 Ok(Statement::ValidateOnly {
3365 kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3366 names,
3367 })
3368 }
3369 Token::Ident(s) | Token::QuotedIdent(s)
3370 if s.eq_ignore_ascii_case("statistics") =>
3371 {
3372 self.parse_drop_statistics_after_drop()
3373 }
3374 other => Err(self.err(format!(
3375 "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3376 SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3377 ))),
3378 }
3379 }
3380 // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3381 // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3382 // and accepted before the view name. SPG materialised
3383 // views re-evaluate on read (always-fresh semantics), so
3384 // the CONCURRENTLY-vs-serial distinction has no runtime
3385 // effect — the refresh body does not block readers either
3386 // way. Same accept-and-no-op pattern as DETACH PARTITION
3387 // CONCURRENTLY (16.5).
3388 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3389 self.advance();
3390 let nxt = self.peek().clone();
3391 if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3392 {
3393 return Err(self.err(alloc::format!(
3394 "expected MATERIALIZED after REFRESH, got {nxt:?}"
3395 )));
3396 }
3397 self.advance();
3398 let nxt2 = self.peek().clone();
3399 if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3400 {
3401 return Err(self.err(alloc::format!(
3402 "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3403 )));
3404 }
3405 self.advance();
3406 // Optional CONCURRENTLY noise word — consumed without
3407 // changing semantics.
3408 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3409 {
3410 self.advance();
3411 }
3412 let name = self.expect_ident_like()?;
3413 let with_data = self.parse_optional_with_data(true)?;
3414 Ok(Statement::RefreshMaterializedView { name, with_data })
3415 }
3416 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3417 self.advance();
3418 self.parse_update_after_keyword()
3419 }
3420 // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3421 // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3422 // [CASCADE | RESTRICT]. Clears every row from each named
3423 // table. Parses at the top level; the engine dispatcher
3424 // walks Statement::Truncate.
3425 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3426 self.advance();
3427 // Optional TABLE noise word — PG accepts both the reserved
3428 // token and the bare identifier spelling.
3429 if matches!(self.peek(), Token::Table)
3430 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3431 {
3432 self.advance();
3433 }
3434 // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3435 // not absorbed. The lookahead keeps a table genuinely
3436 // called `only` working: the keyword is a keyword only
3437 // when a name follows it.
3438 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3439 if s.eq_ignore_ascii_case("only"))
3440 && matches!(
3441 self.tokens.get(self.pos + 1),
3442 Some(Token::Ident(_) | Token::QuotedIdent(_))
3443 );
3444 if only {
3445 self.advance();
3446 }
3447 // Table names (comma-separated).
3448 let mut tables = Vec::new();
3449 loop {
3450 tables.push(self.expect_ident_like()?);
3451 if matches!(self.peek(), Token::Comma) {
3452 self.advance();
3453 continue;
3454 }
3455 break;
3456 }
3457 // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3458 let mut restart_identity = false;
3459 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3460 {
3461 self.advance();
3462 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3463 {
3464 self.advance();
3465 restart_identity = true;
3466 }
3467 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3468 {
3469 self.advance();
3470 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3471 {
3472 self.advance();
3473 }
3474 }
3475 // Optional CASCADE / RESTRICT.
3476 let mut cascade = false;
3477 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3478 {
3479 self.advance();
3480 cascade = true;
3481 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3482 {
3483 self.advance();
3484 }
3485 Ok(Statement::Truncate {
3486 tables,
3487 restart_identity,
3488 cascade,
3489 only,
3490 })
3491 }
3492 // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3493 // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3494 // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3495 // rows change so the index tree is always up-to-date;
3496 // REINDEX is a strict no-op. Accept the whole statement
3497 // shape to boundary for pg_dump round-trip compatibility.
3498 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3499 // v7.39 (round 535) — the target is CARRIED now. SPG has no
3500 // index bloat to rebuild, so the work stays a no-op, but PG
3501 // validates what it was pointed at and this swallowed the
3502 // name at parse time — `REINDEX TABLE typo` reported
3503 // success. Measured on PG18: INDEX / TABLE name a relation,
3504 // SCHEMA a schema, SYSTEM nothing.
3505 self.advance();
3506 self.parse_reindex_tail()
3507 }
3508 // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3509 // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3510 // SPG has no MVCC bloat today (Phase D visibility map
3511 // queues with v7.38); the freezer collapses hot-tier
3512 // rows into cold segments automatically. VACUUM is a
3513 // no-op — pg_dump maintenance scripts and Discourse's
3514 // periodic-maintenance path both emit it.
3515 // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3516 // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3517 // actual bloat, so the pre-MVCC accept-and-ignore posture
3518 // became a silent no-op on a customer's manual reclaim.
3519 // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3520 // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3521 // ANALYZE is captured, the optional table name is captured.
3522 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3523 self.advance();
3524 // Parenthesised option list: absorb it.
3525 if matches!(self.peek(), Token::LParen) {
3526 let mut depth = 0usize;
3527 loop {
3528 match self.advance() {
3529 Token::LParen => depth += 1,
3530 Token::RParen => {
3531 depth -= 1;
3532 if depth == 0 {
3533 break;
3534 }
3535 }
3536 Token::Eof => break,
3537 _ => {}
3538 }
3539 }
3540 }
3541 let mut analyze = false;
3542 let mut table: Option<String> = None;
3543 loop {
3544 match self.peek() {
3545 // v7.39 (round 535) — `FULL` lexes as a keyword, not
3546 // an identifier, so the loop below broke out on it and
3547 // dropped the table name: `VACUUM FULL nosuch` was
3548 // accepted where `VACUUM nosuch` was refused.
3549 Token::Full => {
3550 self.advance();
3551 }
3552 Token::Ident(w) | Token::QuotedIdent(w) => {
3553 let wl = w.to_ascii_lowercase();
3554 match wl.as_str() {
3555 "full" | "freeze" | "verbose" => {
3556 self.advance();
3557 }
3558 "analyze" | "analyse" => {
3559 analyze = true;
3560 self.advance();
3561 }
3562 _ => {
3563 table = Some(self.expect_ident_like()?);
3564 break;
3565 }
3566 }
3567 }
3568 _ => break,
3569 }
3570 }
3571 // Optional trailing column list / anything else to the
3572 // statement boundary (PG accepts per-column ANALYZE).
3573 self.consume_until_statement_boundary();
3574 Ok(Statement::Vacuum { table, analyze })
3575 }
3576 // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3577 // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3578 // <index>. PG stores rows in physical order matching
3579 // an index; SPG's hot-tier is append-only + cold-tier
3580 // is segment-frozen, so clustering has no persistent
3581 // effect. Accept-and-no-op for pg_dump compat.
3582 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3583 // v7.39 (round 535) — same as REINDEX above: the relation is
3584 // carried so the engine can refuse one that does not exist.
3585 // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3586 self.advance();
3587 self.parse_cluster_tail()
3588 }
3589 // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3590 // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3591 // optional string payload; UNLISTEN takes a channel or `*`.
3592 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3593 self.advance();
3594 let ch = match self.advance() {
3595 Token::Ident(c) | Token::QuotedIdent(c) => c,
3596 other => {
3597 return Err(self.err(format!(
3598 "expected channel name after LISTEN, got {other:?}"
3599 )));
3600 }
3601 };
3602 Ok(Statement::Listen(ch))
3603 }
3604 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3605 self.advance();
3606 let channel = match self.advance() {
3607 Token::Ident(c) | Token::QuotedIdent(c) => c,
3608 other => {
3609 return Err(self.err(format!(
3610 "expected channel name after NOTIFY, got {other:?}"
3611 )));
3612 }
3613 };
3614 let payload = if matches!(self.peek(), Token::Comma) {
3615 self.advance();
3616 match self.advance() {
3617 Token::String(p) => Some(p),
3618 other => {
3619 return Err(self.err(format!(
3620 "expected string payload after NOTIFY <channel>, got {other:?}"
3621 )));
3622 }
3623 }
3624 } else {
3625 None
3626 };
3627 Ok(Statement::Notify { channel, payload })
3628 }
3629 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3630 self.advance();
3631 match self.advance() {
3632 Token::Star => Ok(Statement::Unlisten(None)),
3633 Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3634 other => Err(self.err(format!(
3635 "expected channel name or * after UNLISTEN, got {other:?}"
3636 ))),
3637 }
3638 }
3639 // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3640 // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3641 // process-wide write lock today; explicit LOCK has no
3642 // effect. Accept-and-no-op for pg_dump / migration
3643 // compat.
3644 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3645 self.advance();
3646 // v7.39 (round 696) — the LOCK still has no effect (SPG's
3647 // engine holds a process-wide write lock), but the TABLE
3648 // NAME is now carried out so the engine can refuse one that
3649 // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3650 // READ|WRITE` is a different statement with the same first
3651 // word; it keeps the old no-op, because a MySQL dump's
3652 // bracket names tables it is about to create.
3653 let mysql_tables = matches!(self.peek(), Token::Ident(k)
3654 if k.eq_ignore_ascii_case("tables"));
3655 if mysql_tables {
3656 self.consume_until_statement_boundary();
3657 return Ok(Statement::Empty);
3658 }
3659 if matches!(self.peek(), Token::Table) {
3660 self.advance();
3661 }
3662 let names = self.take_comma_separated_names();
3663 self.consume_until_statement_boundary();
3664 Ok(Statement::ValidateOnly {
3665 kind: crate::ast::ValidateOnlyKind::LockTable,
3666 names,
3667 })
3668 }
3669 // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3670 // durability marker + snapshot in PG. SPG has WAL
3671 // checkpointing on a byte / time schedule (v7.37.10
3672 // 60s / 4 MiB defaults). The bare statement parses to
3673 // `Statement::Empty` here (the no_std engine owns no
3674 // WAL / snapshot); v7.37 Epic Du wires the HOST
3675 // (embedded `Database::execute_buffered`, via
3676 // `sql_is_checkpoint`) to force an immediate synchronous
3677 // checkpoint through `Database::checkpoint` — a real
3678 // durability barrier, matching PG.
3679 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3680 self.advance();
3681 self.consume_until_statement_boundary();
3682 Ok(Statement::Empty)
3683 }
3684 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3685 self.advance();
3686 self.parse_delete_after_keyword()
3687 }
3688 // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3689 // ALTER is not a reserved keyword in the lexer — handled
3690 // as a bare ident here.
3691 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3692 self.advance();
3693 self.parse_alter_after_keyword()
3694 }
3695 // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3696 // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3697 // additions needed.
3698 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3699 self.advance();
3700 self.parse_wait_after_keyword()
3701 }
3702 // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3703 // Bare ANALYZE → analyse every user table; ANALYZE
3704 // <name> → re-stats one. The argument is an optional
3705 // ident (or quoted ident); anything else is a parse
3706 // error.
3707 // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3708 // `WHERE` filter (carved out per V6_7_DESIGN.md
3709 // STABILITY). Lex order: identifier "compact" → "cold"
3710 // → "segments". Anything else after `COMPACT` is a
3711 // parse error.
3712 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3713 self.advance();
3714 let next = self.peek().clone();
3715 let cold = match next {
3716 Token::Ident(s) | Token::QuotedIdent(s) => s,
3717 _ => {
3718 return Err(
3719 self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3720 );
3721 }
3722 };
3723 if !cold.eq_ignore_ascii_case("cold") {
3724 return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3725 }
3726 self.advance();
3727 let next = self.peek().clone();
3728 let segments = match next {
3729 Token::Ident(s) | Token::QuotedIdent(s) => s,
3730 _ => {
3731 return Err(self.err(format!(
3732 "expected SEGMENTS after COMPACT COLD, got {:?}",
3733 self.peek()
3734 )));
3735 }
3736 };
3737 if !segments.eq_ignore_ascii_case("segments") {
3738 return Err(self.err(format!(
3739 "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3740 )));
3741 }
3742 self.advance();
3743 Ok(Statement::CompactColdSegments)
3744 }
3745 // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3746 // Parsed as a case-insensitive identifier since MERGE
3747 // isn't a reserved lexer keyword (collides with the
3748 // mysqldump `ALGORITHM = MERGE` view clause if it
3749 // were); the inner parser drives the rest of the
3750 // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3751 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3752 self.advance();
3753 self.parse_merge_after_keyword()
3754 }
3755 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3756 self.advance();
3757 let target = match self.peek() {
3758 Token::Eof | Token::Semicolon => None,
3759 Token::Ident(_) | Token::QuotedIdent(_) => {
3760 Some(self.expect_ident_like()?)
3761 }
3762 other => {
3763 return Err(self.err(format!(
3764 "expected table name or end of statement after ANALYZE, got {other:?}"
3765 )));
3766 }
3767 };
3768 // v7.39 (round 776, F31 J7) — the per-column form
3769 // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
3770 // here while the VACUUM arm already consumed it; SPG
3771 // analyzes whole tables, so the list parses and is
3772 // accepted like the VACUUM path's.
3773 if target.is_some() && matches!(self.peek(), Token::LParen) {
3774 self.advance();
3775 loop {
3776 let _ = self.expect_ident_like()?;
3777 match self.peek() {
3778 Token::Comma => {
3779 self.advance();
3780 }
3781 Token::RParen => {
3782 self.advance();
3783 break;
3784 }
3785 other => {
3786 return Err(self.err(format!(
3787 "expected ',' or ')' in ANALYZE column list, got {other:?}"
3788 )));
3789 }
3790 }
3791 }
3792 }
3793 Ok(Statement::Analyze(target))
3794 }
3795 // v7.12.1 — `SET <name> [TO|=] <value>`. The
3796 // `default_text_search_config` parameter is consumed
3797 // by the FTS function dispatcher; other parameter
3798 // names are recorded but treated as a no-op so PG
3799 // dump output loads.
3800 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
3801 self.advance();
3802 // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
3803 // adds `SET GLOBAL` too (and the alias `SET @@global.name =
3804 // …` which the SessionVar path handles). `LOCAL` is the only
3805 // one that changes semantics — it scopes the change to the
3806 // current transaction — so capture it; SESSION / GLOBAL are
3807 // accepted and treated as the default session scope.
3808 let mut set_local = false;
3809 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
3810 let q = s.to_ascii_lowercase();
3811 if q == "local" || q == "session" || q == "global" {
3812 set_local = q == "local";
3813 self.advance();
3814 }
3815 }
3816 // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
3817 // <collation>]` — change the connection client
3818 // charset. SPG stores UTF-8 always and orders
3819 // bytewise; accept as a no-op.
3820 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
3821 {
3822 self.advance();
3823 // Charset ident-or-string.
3824 if matches!(
3825 self.peek(),
3826 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3827 ) {
3828 self.advance();
3829 }
3830 // Optional `COLLATE <name>`.
3831 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
3832 {
3833 self.advance();
3834 if matches!(
3835 self.peek(),
3836 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
3837 ) {
3838 self.advance();
3839 }
3840 }
3841 return Ok(Statement::Empty);
3842 }
3843 // v7.37.17 (17.6 sibling) — PG `SET ROLE
3844 // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
3845 // uses this to switch to the object owner before
3846 // recreating tables. SPG has no role system so this
3847 // is a no-op.
3848 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
3849 {
3850 self.advance(); // ROLE
3851 // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
3852 // reset to the login identity; a name / string sets the
3853 // effective role that drives current_user + RLS.
3854 let role = match self.peek().clone() {
3855 Token::Default => {
3856 self.advance();
3857 None
3858 }
3859 Token::Ident(s) | Token::QuotedIdent(s)
3860 if s.eq_ignore_ascii_case("none") =>
3861 {
3862 self.advance();
3863 None
3864 }
3865 Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
3866 self.advance();
3867 Some(s)
3868 }
3869 _ => None,
3870 };
3871 return Ok(Statement::SetRole(role));
3872 }
3873 // v7.37.17 (17.6 sibling) — PG `SET SESSION
3874 // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
3875 // ISO SQL surface). pg_dump prepends this to fix
3876 // the isolation level for the restore session. SPG
3877 // defaults to READ COMMITTED and doesn't yet honor
3878 // session-set isolation across statements — accept
3879 // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
3880 // per-tx form is handled elsewhere.
3881 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
3882 {
3883 self.advance(); // CHARACTERISTICS
3884 self.consume_until_statement_boundary();
3885 return Ok(Statement::Empty);
3886 }
3887 // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
3888 // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
3889 // pg_dump emits this to control the deferrability of
3890 // FK / UNIQUE constraints across a bulk restore. SPG
3891 // has no deferrable-constraint machinery today; the
3892 // FK checker is strict-immediate. Accept-and-no-op
3893 // for pg_dump round-trip compatibility.
3894 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
3895 {
3896 self.advance(); // CONSTRAINTS
3897 // v7.39 (round 288) — no longer a no-op: the trailing
3898 // DEFERRED / IMMEDIATE sets the transaction's timing.
3899 // v7.39 (round 308, V29) — and the names are kept.
3900 // They used to be skipped over on the way to the
3901 // DEFERRED keyword, so a named form silently behaved
3902 // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
3903 // every deferrable constraint in the transaction.
3904 let mut names: alloc::vec::Vec<alloc::string::String> =
3905 alloc::vec::Vec::new();
3906 if matches!(self.peek(), Token::All) {
3907 self.advance();
3908 } else {
3909 loop {
3910 let mut n = self.expect_ident_like()?;
3911 // A schema-qualified name (`public.fk_a`)
3912 // identifies the same constraint; PG resolves
3913 // it by the trailing segment.
3914 while matches!(self.peek(), Token::Dot) {
3915 self.advance();
3916 n = self.expect_ident_like()?;
3917 }
3918 names.push(n);
3919 if matches!(self.peek(), Token::Comma) {
3920 self.advance();
3921 } else {
3922 break;
3923 }
3924 }
3925 }
3926 let deferred = match self.peek() {
3927 Token::Ident(s) | Token::QuotedIdent(s)
3928 if s.eq_ignore_ascii_case("deferred") =>
3929 {
3930 true
3931 }
3932 Token::Ident(s) | Token::QuotedIdent(s)
3933 if s.eq_ignore_ascii_case("immediate") =>
3934 {
3935 false
3936 }
3937 other => {
3938 return Err(self.err(alloc::format!(
3939 "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
3940 )));
3941 }
3942 };
3943 self.advance();
3944 return Ok(Statement::SetConstraints { names, deferred });
3945 }
3946 // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
3947 // { DEFAULT | '<role>' | <ident> }` (mailrs
3948 // round-10 A.1). pg_dump preamble emits the
3949 // `DEFAULT` form to reset session authorization.
3950 //
3951 // v7.39 (round 697) — this said "SPG has no role system so
3952 // this is a strict no-op". SPG has had one since round 58;
3953 // the comment outlived it, and with it the reason a name
3954 // that is not a role was accepted here. It still switches
3955 // no authorization — what it does now is refuse a role
3956 // that does not exist, as PG18 does. PG also accepts `RESET SESSION
3957 // AUTHORIZATION` (handled by the RESET parser
3958 // elsewhere). Reference:
3959 // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
3960 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
3961 {
3962 self.advance(); // AUTHORIZATION
3963 match self.peek().clone() {
3964 Token::Default => {
3965 self.advance();
3966 }
3967 Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
3968 self.advance();
3969 return Ok(Statement::ValidateOnly {
3970 kind: crate::ast::ValidateOnlyKind::RoleName,
3971 names: alloc::vec![r],
3972 });
3973 }
3974 other => {
3975 return Err(self.err(alloc::format!(
3976 "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
3977 )));
3978 }
3979 }
3980 return Ok(Statement::Empty);
3981 }
3982 // v7.38 轴 4 — `SET [SESSION] TRANSACTION
3983 // ISOLATION LEVEL { READ COMMITTED | READ
3984 // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
3985 // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
3986 // PG-standard surface. v7.37.8 accepts the syntax
3987 // and tracks the selected level on
3988 // `Engine::current_isolation_level()`; the actual
3989 // MVCC / SSI semantics implementation lands in
3990 // the 轴 4 isolation framework (separate train).
3991 // PG itself maps READ UNCOMMITTED to READ COMMITTED
3992 // internally; SPG behaves the same (effectively
3993 // READ COMMITTED at every level today).
3994 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
3995 {
3996 self.advance(); // TRANSACTION
3997 let level = self.parse_isolation_level_clauses()?.unwrap_or_default();
3998 return Ok(Statement::SetTransaction { isolation: level });
3999 }
4000 // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4001 // alias — same accept-as-no-op as SET NAMES.
4002 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4003 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4004 {
4005 self.advance(); // CHARACTER
4006 self.advance(); // SET
4007 if matches!(
4008 self.peek(),
4009 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4010 ) {
4011 self.advance();
4012 }
4013 return Ok(Statement::Empty);
4014 }
4015 // v7.39 (GUC) — PG spells the timezone GUC as two
4016 // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4017 // where <value> is a string/ident or the LOCAL /
4018 // DEFAULT keyword (both mean "back to the default").
4019 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4020 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4021 {
4022 self.advance(); // TIME
4023 self.advance(); // ZONE
4024 let value = match self.peek().clone() {
4025 Token::Ident(s)
4026 if s.eq_ignore_ascii_case("local")
4027 || s.eq_ignore_ascii_case("default") =>
4028 {
4029 self.advance();
4030 crate::ast::SetValue::Default
4031 }
4032 Token::Default => {
4033 self.advance();
4034 crate::ast::SetValue::Default
4035 }
4036 _ => self.parse_set_value()?,
4037 };
4038 return Ok(Statement::SetParameter {
4039 name: "timezone".into(),
4040 value,
4041 local: set_local,
4042 });
4043 }
4044 // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4045 // MySQL USER-variable assignment: its own per-session
4046 // namespace, an arbitrary expression on the right, and `:=`
4047 // as a second spelling of `=`. It used to fall into the
4048 // session-PARAMETER list below, whose values are literals and
4049 // whose store nothing reads back under a `@` name — so the
4050 // assignment reported success and vanished.
4051 //
4052 // A `@@`-prefixed LHS is a real engine setting and keeps the
4053 // old path.
4054 if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4055 return self.parse_set_user_vars();
4056 }
4057 // v7.14.0 — multi-assignment form
4058 // `SET a = 1, b = 2, …`. Single-assignment is the
4059 // 1-element case. Each LHS may be a regular ident
4060 // or a SessionVar (`@VAR` / `@@VAR`).
4061 let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4062 loop {
4063 let lhs = match self.peek().clone() {
4064 Token::SessionVar(s) => {
4065 self.advance();
4066 s
4067 }
4068 Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4069 other => {
4070 return Err(self.err(format!(
4071 "expected parameter name after SET, got {other:?}"
4072 )));
4073 }
4074 };
4075 // Accept either `=` or the bare `TO` keyword.
4076 match self.peek() {
4077 Token::Eq => {
4078 self.advance();
4079 }
4080 Token::To => {
4081 self.advance();
4082 }
4083 other => {
4084 return Err(self.err(format!(
4085 "expected `=` or TO after SET {lhs}, got {other:?}"
4086 )));
4087 }
4088 }
4089 let mut value = self.parse_set_value()?;
4090 // v7.39 (GUC) — disambiguate the comma: `, name =` /
4091 // `, name TO` continues a MySQL-style multi-assign,
4092 // anything else is a PG list VALUE
4093 // (`SET search_path = myschema, public`) folded into
4094 // one comma-joined string.
4095 while matches!(self.peek(), Token::Comma) {
4096 let is_assign = matches!(
4097 self.tokens.get(self.pos + 1),
4098 Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4099 ) && matches!(
4100 self.tokens.get(self.pos + 2),
4101 Some(Token::Eq | Token::To)
4102 );
4103 if is_assign {
4104 break;
4105 }
4106 self.advance(); // comma
4107 let next = self.parse_set_value()?;
4108 let joined = alloc::format!(
4109 "{}, {}",
4110 set_value_text(&value),
4111 set_value_text(&next)
4112 );
4113 value = crate::ast::SetValue::String(joined);
4114 }
4115 pairs.push((lhs, value));
4116 if matches!(self.peek(), Token::Comma) {
4117 self.advance();
4118 continue;
4119 }
4120 break;
4121 }
4122 if pairs.len() == 1 {
4123 let (name, value) = pairs.into_iter().next().unwrap();
4124 Ok(Statement::SetParameter {
4125 name,
4126 value,
4127 local: set_local,
4128 })
4129 } else {
4130 Ok(Statement::SetParameterList(pairs))
4131 }
4132 }
4133 // v7.12.1 — `RESET <name>` / `RESET ALL`.
4134 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4135 self.advance();
4136 match self.peek().clone() {
4137 Token::All => {
4138 self.advance();
4139 Ok(Statement::ResetParameter(None))
4140 }
4141 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4142 self.advance();
4143 Ok(Statement::ResetParameter(None))
4144 }
4145 // v7.39 (RLS) — `RESET ROLE` clears the session role.
4146 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4147 self.advance();
4148 Ok(Statement::SetRole(None))
4149 }
4150 _ => {
4151 let name = self.parse_set_param_name()?;
4152 Ok(Statement::ResetParameter(Some(name)))
4153 }
4154 }
4155 }
4156 // v7.39 (round 218) — server-side cursors.
4157 Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4158 Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4159 Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4160 Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4161 self.advance();
4162 match self.peek().clone() {
4163 Token::All => {
4164 self.advance();
4165 Ok(Statement::CloseCursor { name: None })
4166 }
4167 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4168 self.advance();
4169 Ok(Statement::CloseCursor { name: None })
4170 }
4171 Token::Ident(n) | Token::QuotedIdent(n) => {
4172 self.advance();
4173 Ok(Statement::CloseCursor { name: Some(n) })
4174 }
4175 other => Err(self.err(format!(
4176 "expected cursor name or ALL after CLOSE, got {other:?}"
4177 ))),
4178 }
4179 }
4180 other => Err(self.err(format!(
4181 "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4182 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4183 ))),
4184 }
4185 }
4186
4187 /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4188 /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4189 /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4190 /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4191 fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4192 self.advance(); // DECLARE
4193 let name = match self.advance() {
4194 Token::Ident(n) | Token::QuotedIdent(n) => n,
4195 other => {
4196 return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4197 }
4198 };
4199 let mut scroll: Option<bool> = None;
4200 loop {
4201 match self.peek() {
4202 Token::Ident(s)
4203 if s.eq_ignore_ascii_case("binary")
4204 || s.eq_ignore_ascii_case("insensitive")
4205 || s.eq_ignore_ascii_case("asensitive") =>
4206 {
4207 self.advance();
4208 }
4209 Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4210 self.advance();
4211 scroll = Some(true);
4212 }
4213 Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4214 {
4215 self.advance(); // NO
4216 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4217 return Err(self.err(format!(
4218 "expected SCROLL after NO in DECLARE, got {:?}",
4219 self.peek()
4220 )));
4221 }
4222 self.advance();
4223 scroll = Some(false);
4224 }
4225 _ => break,
4226 }
4227 }
4228 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4229 return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4230 }
4231 self.advance();
4232 let mut hold = false;
4233 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4234 self.advance();
4235 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4236 return Err(self.err(format!(
4237 "expected HOLD after WITH in DECLARE, got {:?}",
4238 self.peek()
4239 )));
4240 }
4241 self.advance();
4242 hold = true;
4243 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4244 self.advance();
4245 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4246 return Err(self.err(format!(
4247 "expected HOLD after WITHOUT in DECLARE, got {:?}",
4248 self.peek()
4249 )));
4250 }
4251 self.advance();
4252 }
4253 if !matches!(self.peek(), Token::For) {
4254 return Err(self.err(format!(
4255 "expected FOR before the cursor query, got {:?}",
4256 self.peek()
4257 )));
4258 }
4259 self.advance();
4260 let query = self.parse_one_statement()?;
4261 Ok(Statement::DeclareCursor {
4262 name,
4263 scroll,
4264 hold,
4265 query: alloc::boxed::Box::new(query),
4266 })
4267 }
4268
4269 /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4270 /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4271 /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4272 fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4273 use crate::ast::CursorDirection as D;
4274 self.advance(); // FETCH / MOVE
4275 let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4276 let neg = if matches!(this.peek(), Token::Minus) {
4277 this.advance();
4278 true
4279 } else {
4280 false
4281 };
4282 match this.advance() {
4283 Token::Integer(v) => Ok(if neg { -v } else { v }),
4284 other => Err(this.err(format!("expected count, got {other:?}"))),
4285 }
4286 };
4287 let direction = match self.peek().clone() {
4288 Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4289 self.advance();
4290 D::Next
4291 }
4292 Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4293 self.advance();
4294 D::Prior
4295 }
4296 Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4297 self.advance();
4298 D::First
4299 }
4300 Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4301 self.advance();
4302 D::Last
4303 }
4304 Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4305 self.advance();
4306 D::Absolute(signed_count(self)?)
4307 }
4308 Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4309 self.advance();
4310 D::Relative(signed_count(self)?)
4311 }
4312 Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4313 self.advance();
4314 match self.peek().clone() {
4315 Token::All => {
4316 self.advance();
4317 D::All
4318 }
4319 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4320 self.advance();
4321 D::All
4322 }
4323 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4324 _ => D::Next, // bare FORWARD = FORWARD 1
4325 }
4326 }
4327 Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4328 self.advance();
4329 match self.peek().clone() {
4330 Token::All => {
4331 self.advance();
4332 D::BackwardAll
4333 }
4334 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4335 self.advance();
4336 D::BackwardAll
4337 }
4338 Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4339 _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4340 }
4341 }
4342 Token::All => {
4343 self.advance();
4344 D::All
4345 }
4346 Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4347 self.advance();
4348 D::All
4349 }
4350 Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4351 // Bare `FETCH <name>` — direction defaults to NEXT.
4352 _ => D::Next,
4353 };
4354 // Optional FROM / IN.
4355 if matches!(self.peek(), Token::From)
4356 || matches!(self.peek(), Token::In)
4357 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4358 {
4359 self.advance();
4360 }
4361 let name = match self.advance() {
4362 Token::Ident(n) | Token::QuotedIdent(n) => n,
4363 other => {
4364 return Err(self.err(format!("expected cursor name, got {other:?}")));
4365 }
4366 };
4367 Ok(if is_move {
4368 Statement::MoveCursor { name, direction }
4369 } else {
4370 Statement::FetchCursor { name, direction }
4371 })
4372 }
4373
4374 /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4375 /// [(kind, …)] ON <col>, … FROM <table>`.
4376 fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4377 self.advance(); // STATISTICS
4378 // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4379 let mut if_not_exists = false;
4380 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4381 && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4382 {
4383 self.advance();
4384 self.advance();
4385 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4386 self.advance();
4387 if_not_exists = true;
4388 }
4389 }
4390 let name = self.expect_ident_like()?;
4391 let mut kinds = Vec::new();
4392 if matches!(self.peek(), Token::LParen) {
4393 self.advance();
4394 loop {
4395 let k = self.expect_ident_like()?;
4396 // PG stores the single letters; accept the spelled-out
4397 // names the SQL uses and record what PG records.
4398 kinds.push(match k.to_ascii_lowercase().as_str() {
4399 "ndistinct" => String::from("d"),
4400 "dependencies" => String::from("f"),
4401 "mcv" => String::from("m"),
4402 other => {
4403 return Err(
4404 self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4405 );
4406 }
4407 });
4408 match self.advance() {
4409 Token::Comma => {}
4410 Token::RParen => break,
4411 other => {
4412 return Err(self.err(alloc::format!(
4413 "expected ',' or ')' in statistics kind list, got {other:?}"
4414 )));
4415 }
4416 }
4417 }
4418 }
4419 if !matches!(self.peek(), Token::On) {
4420 return Err(self.err(alloc::format!(
4421 "expected ON in CREATE STATISTICS, got {:?}",
4422 self.peek()
4423 )));
4424 }
4425 self.advance();
4426 let mut columns = Vec::new();
4427 loop {
4428 columns.push(self.expect_ident_like()?);
4429 if matches!(self.peek(), Token::Comma) {
4430 self.advance();
4431 } else {
4432 break;
4433 }
4434 }
4435 if !matches!(self.peek(), Token::From) {
4436 return Err(self.err(alloc::format!(
4437 "expected FROM in CREATE STATISTICS, got {:?}",
4438 self.peek()
4439 )));
4440 }
4441 self.advance();
4442 let table = self.expect_ident_like()?;
4443 Ok(Statement::CreateStatistics {
4444 name,
4445 if_not_exists,
4446 kinds,
4447 columns,
4448 table,
4449 })
4450 }
4451
4452 /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4453 /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4454 /// entered with the `TABLE` keyword still unconsumed. Extracted so
4455 /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4456 /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4457 /// forward call.
4458 fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4459 self.advance(); // TABLE
4460 let if_exists = self.consume_if_exists();
4461 let mut names: Vec<String> = Vec::new();
4462 loop {
4463 names.push(self.expect_ident_like()?);
4464 if matches!(self.peek(), Token::Comma) {
4465 self.advance();
4466 continue;
4467 }
4468 break;
4469 }
4470 if matches!(
4471 self.peek(),
4472 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4473 || s.eq_ignore_ascii_case("restrict")
4474 ) {
4475 self.advance();
4476 }
4477 Ok(Statement::DropTable { names, if_exists })
4478 }
4479
4480 fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4481 self.advance(); // STATISTICS
4482 let mut if_exists = false;
4483 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4484 && matches!(self.tokens.get(self.pos + 1),
4485 Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4486 {
4487 self.advance();
4488 self.advance();
4489 if_exists = true;
4490 }
4491 let name = self.expect_ident_like()?;
4492 Ok(Statement::DropStatistics { name, if_exists })
4493 }
4494
4495 fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4496 debug_assert!(matches!(self.peek(), Token::Create));
4497 self.advance();
4498 match self.peek() {
4499 Token::Table => self.parse_create_table_stmt_after_create(),
4500 Token::Index => self.parse_create_index_stmt_after_create(false),
4501 // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4502 // object now. It used to be consumed by the CREATE-noise
4503 // arm, so a pg_dump that declares extended statistics
4504 // restored silently without them and reflection showed
4505 // nothing.
4506 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4507 self.parse_create_statistics_after_create()
4508 }
4509 // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4510 // The `UNIQUE` modifier turns a partial index into a
4511 // partial-uniqueness invariant (only rows matching the
4512 // WHERE predicate are checked for duplicates). mailrs
4513 // K1 (3 hits: email_templates default, calendar_events
4514 // master, calendar_events instance).
4515 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4516 self.advance();
4517 if !matches!(self.peek(), Token::Index) {
4518 return Err(self.err(alloc::format!(
4519 "expected INDEX after CREATE UNIQUE, got {:?}",
4520 self.peek()
4521 )));
4522 }
4523 self.parse_create_index_stmt_after_create(true)
4524 }
4525 Token::Publication => {
4526 self.advance();
4527 self.parse_create_publication_after_keyword()
4528 }
4529 Token::Subscription => {
4530 self.advance();
4531 self.parse_create_subscription_after_keyword()
4532 }
4533 // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4534 // USER isn't a reserved keyword — we look for the bare
4535 // identifier so the lexer doesn't have to grow a token.
4536 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4537 self.advance();
4538 self.parse_create_user_after_keyword(true)
4539 }
4540 // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4541 // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4542 // the default of the LOGIN attribute.
4543 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4544 self.advance();
4545 self.parse_create_user_after_keyword(false)
4546 }
4547 // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4548 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4549 self.advance();
4550 self.parse_create_policy_after_keyword()
4551 }
4552 // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4553 // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4554 // no-op. mailrs follow-up F3.
4555 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4556 self.advance();
4557 self.parse_create_extension_after_keyword()
4558 }
4559 // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4560 // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4561 // optional; absorb it here and forward to the
4562 // per-kind parsers with the flag. OR is a reserved
4563 // keyword token.
4564 Token::Or => {
4565 self.advance();
4566 let next = self.peek();
4567 let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4568 return Err(self.err(alloc::format!(
4569 "expected REPLACE after CREATE OR, got {next:?}"
4570 )));
4571 };
4572 if !s2.eq_ignore_ascii_case("replace") {
4573 return Err(self.err(alloc::format!(
4574 "expected REPLACE after CREATE OR, got {s2:?}"
4575 )));
4576 }
4577 self.advance();
4578 self.parse_create_function_or_trigger_after_or_replace(true)
4579 }
4580 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4581 self.advance();
4582 self.parse_create_function_after_keyword(false)
4583 }
4584 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4585 self.advance();
4586 self.parse_create_trigger_after_keyword(false)
4587 }
4588 // v7.39 (round 139) — CREATE RULE …
4589 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4590 self.advance();
4591 self.parse_create_rule_after_keyword(false)
4592 }
4593 // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4594 // trigger is a row-level AFTER trigger that additionally carries
4595 // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4596 // path already tolerates and skips those clauses, so consuming the
4597 // CONSTRAINT keyword and reusing it makes the statement parse and the
4598 // trigger fire. (The deferral timing itself is not yet honoured —
4599 // SPG fires it as a plain AFTER trigger, which is correct behaviour
4600 // for every non-deferred use.)
4601 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4602 self.advance();
4603 if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4604 if t.eq_ignore_ascii_case("trigger"))
4605 {
4606 return Err(self.err(alloc::format!(
4607 "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4608 self.peek()
4609 )));
4610 }
4611 self.advance();
4612 self.parse_create_trigger_after_keyword(false)
4613 }
4614 // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4615 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4616 self.advance();
4617 self.parse_create_sequence_after_keyword(false)
4618 }
4619 // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4620 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4621 self.advance();
4622 self.parse_create_view_after_keyword(false, false, false)
4623 }
4624 // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4625 // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4626 // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4627 // appear (in any order) between `CREATE` and `VIEW` in
4628 // every mysqldump-emitted view. Pre-2.6 the parser
4629 // rejected the prefix and the customer's whole view
4630 // backup failed on the first view. The hints are pure
4631 // planner / permission metadata; SPG's view-rewrite
4632 // path is semantically equivalent for all three
4633 // algorithms in v7.17 (TEMPTABLE differs only in
4634 // perf for huge views — out of v7.17 scope), and
4635 // DEFINER / SQL SECURITY are pure single-user
4636 // permissioning that SPG ignores by design.
4637 Token::Ident(s) | Token::QuotedIdent(s)
4638 if s.eq_ignore_ascii_case("algorithm")
4639 || s.eq_ignore_ascii_case("definer")
4640 || s.eq_ignore_ascii_case("sql") =>
4641 {
4642 self.consume_mysql_view_prefix()?;
4643 // After absorbing ALGORITHM / DEFINER / SQL SECURITY
4644 // (in any order, in any combination), the next
4645 // keyword must be VIEW. mysqldump never emits these
4646 // prefixes on non-view statements.
4647 let next = self.peek().clone();
4648 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
4649 if s2.eq_ignore_ascii_case("view"))
4650 {
4651 self.advance();
4652 self.parse_create_view_after_keyword(false, false, false)
4653 } else {
4654 Err(self.err(alloc::format!(
4655 "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
4656 )))
4657 }
4658 }
4659 // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
4660 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
4661 self.advance();
4662 self.parse_create_type_after_keyword()
4663 }
4664 // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
4665 // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
4666 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
4667 self.advance();
4668 self.parse_create_domain_after_keyword()
4669 }
4670 // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
4671 // name [AUTHORIZATION user]. Real catalog registry
4672 // (was silent-no-op'd pre-v7.17).
4673 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
4674 self.advance();
4675 let if_not_exists = self.parse_if_not_exists();
4676 let name = self.expect_ident_like()?;
4677 // Optional `AUTHORIZATION <user>` trailer — accepted,
4678 // ignored (single-user catalog).
4679 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4680 if s.eq_ignore_ascii_case("authorization"))
4681 {
4682 self.advance();
4683 let _ = self.expect_ident_like()?;
4684 }
4685 Ok(Statement::CreateSchema { name, if_not_exists })
4686 }
4687 // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
4688 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
4689 self.advance();
4690 let next = self.peek().clone();
4691 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4692 {
4693 self.advance();
4694 self.parse_create_materialized_view_after_keyword()
4695 } else {
4696 Err(self.err(alloc::format!(
4697 "expected VIEW after CREATE MATERIALIZED, got {next:?}"
4698 )))
4699 }
4700 }
4701 // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
4702 // no-op below), an UNLOGGED table is a real, fully-usable table in
4703 // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
4704 // durability optimisation is a follow-up), so a dump / app that
4705 // declares UNLOGGED tables works instead of failing to parse.
4706 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
4707 self.advance(); // UNLOGGED
4708 if matches!(self.peek(), Token::Table) {
4709 self.parse_create_table_stmt_after_create()
4710 } else {
4711 Err(self.err(format!(
4712 "expected TABLE after CREATE UNLOGGED, got {:?}",
4713 self.peek()
4714 )))
4715 }
4716 }
4717 Token::Ident(s) | Token::QuotedIdent(s)
4718 if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
4719 {
4720 self.advance();
4721 // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
4722 let next = self.peek().clone();
4723 if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
4724 {
4725 self.advance();
4726 self.parse_create_sequence_after_keyword(true)
4727 } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
4728 {
4729 self.advance();
4730 self.parse_create_view_after_keyword(false, false, true)
4731 } else {
4732 // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
4733 // consumed and answered OK while creating nothing, so
4734 // every statement that touched the table afterwards failed
4735 // with "table not found" — the DDL itself lied. It is a
4736 // real CREATE TABLE now, marked temporary so the executor
4737 // puts it in the session's own namespace. An optional
4738 // TABLE keyword may or may not be present (`CREATE TEMP t`
4739 // is not legal, but the keyword is consumed by the
4740 // CREATE TABLE parser itself).
4741 let stmt = self.parse_create_table_stmt_after_create()?;
4742 match stmt {
4743 Statement::CreateTable(mut c) => {
4744 c.temporary = true;
4745 Ok(Statement::CreateTable(c))
4746 }
4747 // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
4748 // CTAS node, which needs the same session namespace.
4749 Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
4750 m.temporary = true;
4751 Ok(Statement::CreateMaterializedView(m))
4752 }
4753 other => Ok(other),
4754 }
4755 }
4756 }
4757 // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
4758 // BEGIN <body> END`. The body may reference `@var`
4759 // session variables, SET statements, internal `;`
4760 // terminators, etc. SPG has no procedure runtime, so
4761 // consume the whole `CREATE PROCEDURE … END` block as
4762 // a no-op so mysqldump scripts that include stored
4763 // routines load through. The matching-END consumer
4764 // tracks BEGIN/END nesting depth to handle nested
4765 // BEGIN blocks correctly.
4766 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
4767 self.consume_mysql_routine_body();
4768 Ok(Statement::Empty)
4769 }
4770 // v7.14.0 — pg_dump / mysqldump emit
4771 // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
4772 // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
4773 // SPG is single-schema / single-database; these have
4774 // no behavioural effect, so consume + return Empty.
4775 // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
4776 // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
4777 // moved up to real parser branches. DATABASE / ROLE /
4778 // POLICY / OPERATOR stay no-op forever
4779 // (single-database, hardcoded roles).
4780 Token::Ident(s) | Token::QuotedIdent(s)
4781 if matches!(
4782 s.to_ascii_lowercase().as_str(),
4783 "database"
4784 | "role"
4785 | "operator"
4786 | "cast"
4787 | "aggregate"
4788 | "language"
4789 | "collation"
4790 | "conversion"
4791 // v7.17.0 Phase 8 (audit N6) — rarely-
4792 // emitted pg_dump shapes that should
4793 // load through without a parser error.
4794 // SPG has no planner statistics catalog,
4795 // no event-trigger hooks, no foreign-
4796 // data-wrapper infrastructure; consume
4797 // + return Empty.
4798 | "statistics"
4799 | "event"
4800 // v7.37.17 (17.6 siblings) — additional CREATE
4801 // targets pg_dump / operator install scripts
4802 // may emit that SPG has no matching machinery
4803 // for. Consume + Empty-return.
4804 | "text"
4805 | "tablespace"
4806 | "access"
4807 | "large"
4808 ) =>
4809 {
4810 // DATABASE is the one member of this list PG refuses
4811 // inside a transaction block; the rest (ROLE, CAST,
4812 // TABLESPACE, …) it runs there quite happily, so only
4813 // this one is named. Still a no-op otherwise — SPG is
4814 // single-database.
4815 let is_database = s.eq_ignore_ascii_case("database");
4816 self.consume_until_statement_boundary();
4817 if is_database {
4818 return Ok(Statement::NoOpPreventedInTransaction {
4819 what: String::from("CREATE DATABASE"),
4820 });
4821 }
4822 Ok(Statement::Empty)
4823 }
4824 // v7.39 (round 706) — the foreign-data family leaves the silent
4825 // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
4826 // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
4827 // FDW machinery), but the ENGINE now warns, so a restore log
4828 // says what will not function instead of reporting success.
4829 Token::Ident(s) | Token::QuotedIdent(s)
4830 if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
4831 {
4832 self.consume_until_statement_boundary();
4833 Ok(Statement::ValidateOnly {
4834 kind: crate::ast::ValidateOnlyKind::ForeignInfra,
4835 names: Vec::new(),
4836 })
4837 }
4838 other => Err(self.err(format!(
4839 "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
4840 ))),
4841 }
4842 }
4843
4844 /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
4845 /// keyword decides whether we parse a function or trigger
4846 /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
4847 /// PROCEDURE) — those land in later releases.
4848 fn parse_create_function_or_trigger_after_or_replace(
4849 &mut self,
4850 or_replace: bool,
4851 ) -> Result<Statement, ParseError> {
4852 let tok = self.peek();
4853 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4854 return Err(self.err(alloc::format!(
4855 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
4856 )));
4857 };
4858 if s.eq_ignore_ascii_case("function") {
4859 self.advance();
4860 self.parse_create_function_after_keyword(or_replace)
4861 } else if s.eq_ignore_ascii_case("trigger") {
4862 self.advance();
4863 self.parse_create_trigger_after_keyword(or_replace)
4864 } else if s.eq_ignore_ascii_case("rule") {
4865 // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
4866 self.advance();
4867 self.parse_create_rule_after_keyword(or_replace)
4868 } else if s.eq_ignore_ascii_case("view") {
4869 // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
4870 self.advance();
4871 self.parse_create_view_after_keyword(or_replace, false, false)
4872 } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
4873 // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
4874 self.advance();
4875 let nxt = self.peek().clone();
4876 if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
4877 {
4878 self.advance();
4879 self.parse_create_view_after_keyword(or_replace, false, true)
4880 } else {
4881 Err(self.err(alloc::format!(
4882 "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
4883 )))
4884 }
4885 } else {
4886 Err(self.err(alloc::format!(
4887 "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
4888 )))
4889 }
4890 }
4891
4892 /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
4893 /// SPG doesn't have a registry; pgvector / similar are
4894 /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
4895 /// the syntax lets dual-target schemas keep the line.
4896 fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
4897 // Optional `IF NOT EXISTS`.
4898 self.consume_if_not_exists();
4899 let name = self.expect_ident_like()?;
4900 // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
4901 // CASCADE / FROM '<v>' clauses; we don't model them.
4902 loop {
4903 match self.peek() {
4904 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
4905 self.advance();
4906 continue;
4907 }
4908 Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
4909 self.advance();
4910 let _ = self.expect_ident_like()?;
4911 continue;
4912 }
4913 Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
4914 self.advance();
4915 // String or ident literal.
4916 let _ = self.advance();
4917 continue;
4918 }
4919 Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
4920 self.advance();
4921 let _ = self.advance();
4922 continue;
4923 }
4924 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
4925 self.advance();
4926 continue;
4927 }
4928 _ => break,
4929 }
4930 }
4931 // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
4932 // nosuch` reported success and `pg_extension` then did not list it,
4933 // which is the accept-and-do-nothing shape F31 exists to find.
4934 Ok(Statement::ValidateOnly {
4935 kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
4936 names: alloc::vec![name],
4937 })
4938 }
4939
4940 /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
4941 /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
4942 /// already been consumed by the caller. Grammar accepted:
4943 ///
4944 /// name `(` arg-list `)`
4945 /// `RETURNS` return-type
4946 /// [ `LANGUAGE` ident ]
4947 /// `AS` $$ body $$
4948 /// [ `LANGUAGE` ident ]
4949 ///
4950 /// Either `LANGUAGE` position is allowed; PG accepts both.
4951 fn parse_create_function_after_keyword(
4952 &mut self,
4953 or_replace: bool,
4954 ) -> Result<Statement, ParseError> {
4955 let name = self.expect_ident_like()?;
4956 // Argument list. v7.12.4 commonly sees the empty `()`
4957 // (trigger functions); typed args parse and round-trip
4958 // but the executor only invokes nullary functions.
4959 if !matches!(self.peek(), Token::LParen) {
4960 return Err(self.err(alloc::format!(
4961 "expected '(' after function name {name:?}, got {:?}",
4962 self.peek()
4963 )));
4964 }
4965 self.advance();
4966 let args = self.parse_function_arg_list()?;
4967 // RETURNS clause.
4968 let tok = self.peek();
4969 let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
4970 return Err(self.err(alloc::format!(
4971 "expected RETURNS after function arg list, got {tok:?}"
4972 )));
4973 };
4974 if !s.eq_ignore_ascii_case("returns") {
4975 return Err(self.err(alloc::format!(
4976 "expected RETURNS after function arg list, got {s:?}"
4977 )));
4978 }
4979 self.advance();
4980 let returns = self.parse_function_return()?;
4981 // Optional LANGUAGE clause (PG also accepts after AS — we'll
4982 // re-check after the body too).
4983 let mut language: Option<String> = self.parse_optional_language()?;
4984 // v7.39 (round 322, V46) — attribute clauses. PG allows them on
4985 // either side of the body and in any order, interleaved with
4986 // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
4987 // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
4988 // PG's own pg_dump output did not restore.
4989 let mut attrs = FunctionAttrs::default();
4990 loop {
4991 let before = self.pos;
4992 self.parse_function_attrs_into(&mut attrs)?;
4993 if language.is_none() {
4994 language = self.parse_optional_language()?;
4995 }
4996 if self.pos == before {
4997 break;
4998 }
4999 }
5000 // `AS` followed by a $$-quoted body (lexer already
5001 // collapses both `$$…$$` and `$tag$…$tag$` to a single
5002 // Token::String). AS is a reserved keyword (Token::As).
5003 if !matches!(self.peek(), Token::As) {
5004 return Err(self.err(alloc::format!(
5005 "expected AS before function body, got {:?}",
5006 self.peek()
5007 )));
5008 }
5009 self.advance();
5010 let body_text = match self.peek() {
5011 Token::String(s) => {
5012 let body = s.clone();
5013 self.advance();
5014 body
5015 }
5016 other => {
5017 return Err(self.err(alloc::format!(
5018 "expected $$-quoted function body after AS, got {other:?}"
5019 )));
5020 }
5021 };
5022 // Trailing clauses — PG's other accepted position for both the
5023 // LANGUAGE and the attributes.
5024 loop {
5025 let before = self.pos;
5026 self.parse_function_attrs_into(&mut attrs)?;
5027 if language.is_none() {
5028 language = self.parse_optional_language()?;
5029 }
5030 if self.pos == before {
5031 break;
5032 }
5033 }
5034 let language = language.unwrap_or_else(|| String::from("sql"));
5035 // PL/pgSQL bodies get structure-parsed. Other languages
5036 // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5037 // recognise) round-trip as Raw text — the executor errors
5038 // when invoked with a clear unsupported message.
5039 let body = if language.eq_ignore_ascii_case("plpgsql") {
5040 match parse_plpgsql_body(&body_text) {
5041 Ok(block) => FunctionBody::PlPgSql(block),
5042 // Best-effort: if the body parser doesn't yet
5043 // support a construct used inside, fall back to
5044 // raw — keeps `CREATE FUNCTION` itself working
5045 // (catalogue accepts), executor errors on
5046 // invocation only.
5047 Err(_) => FunctionBody::Raw(body_text),
5048 }
5049 } else {
5050 FunctionBody::Raw(body_text)
5051 };
5052 Ok(Statement::CreateFunction(CreateFunctionStatement {
5053 name,
5054 or_replace,
5055 args,
5056 returns,
5057 language,
5058 body,
5059 attrs,
5060 }))
5061 }
5062
5063 /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5064 /// attribute clauses into `attrs`, stopping at the first token that
5065 /// is not one. Measured against PG 18.4, which accepts them in any
5066 /// order and on either side of the body.
5067 fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5068 loop {
5069 let word = match self.peek() {
5070 Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5071 // NOT LEAKPROOF — NOT is a reserved keyword token.
5072 Token::Not
5073 if matches!(
5074 self.tokens.get(self.pos + 1),
5075 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5076 ) =>
5077 {
5078 self.advance();
5079 self.advance();
5080 attrs.leakproof = false;
5081 continue;
5082 }
5083 _ => return Ok(()),
5084 };
5085 match word.as_str() {
5086 "immutable" => {
5087 self.advance();
5088 attrs.volatility = FunctionVolatility::Immutable;
5089 }
5090 "stable" => {
5091 self.advance();
5092 attrs.volatility = FunctionVolatility::Stable;
5093 }
5094 "volatile" => {
5095 self.advance();
5096 attrs.volatility = FunctionVolatility::Volatile;
5097 }
5098 "strict" => {
5099 self.advance();
5100 attrs.strict = true;
5101 }
5102 "leakproof" => {
5103 self.advance();
5104 attrs.leakproof = true;
5105 }
5106 // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5107 // spelled-out forms of STRICT and its opposite.
5108 "returns" | "called" => {
5109 let strict = word == "returns";
5110 let mut probe = self.pos + 1;
5111 if strict {
5112 // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5113 // is not ours.
5114 match self.tokens.get(probe) {
5115 Some(Token::Null) => probe += 1,
5116 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5117 _ => return Ok(()),
5118 }
5119 }
5120 let ok = matches!(self.tokens.get(probe), Some(Token::On))
5121 || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5122 if !ok {
5123 return Ok(());
5124 }
5125 probe += 1;
5126 match self.tokens.get(probe) {
5127 Some(Token::Null) => probe += 1,
5128 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5129 _ => return Ok(()),
5130 }
5131 match self.tokens.get(probe) {
5132 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5133 _ => return Ok(()),
5134 }
5135 self.pos = probe;
5136 attrs.strict = strict;
5137 }
5138 "security" | "external" => {
5139 // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5140 let mut probe = self.pos + 1;
5141 if word == "external" {
5142 match self.tokens.get(probe) {
5143 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5144 probe += 1;
5145 }
5146 _ => return Ok(()),
5147 }
5148 }
5149 let definer = match self.tokens.get(probe) {
5150 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5151 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5152 _ => return Ok(()),
5153 };
5154 self.pos = probe + 1;
5155 attrs.security_definer = definer;
5156 }
5157 "parallel" => {
5158 let level = match self.tokens.get(self.pos + 1) {
5159 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5160 FunctionParallel::Safe
5161 }
5162 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5163 FunctionParallel::Restricted
5164 }
5165 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5166 FunctionParallel::Unsafe
5167 }
5168 _ => return Ok(()),
5169 };
5170 self.pos += 2;
5171 attrs.parallel = level;
5172 }
5173 "cost" | "rows" => {
5174 let Some(n) = self.peek_number_at(self.pos + 1) else {
5175 return Ok(());
5176 };
5177 self.pos += 2;
5178 if word == "cost" {
5179 attrs.cost = Some(n);
5180 } else {
5181 attrs.rows = Some(n);
5182 }
5183 }
5184 _ => return Ok(()),
5185 }
5186 }
5187 }
5188
5189 /// The numeric literal at `idx`, if there is one.
5190 fn peek_number_at(&self, idx: usize) -> Option<f64> {
5191 match self.tokens.get(idx)? {
5192 Token::Integer(n) => Some(*n as f64),
5193 Token::Float(f) => Some(*f),
5194 Token::Numeric(t) => t.parse::<f64>().ok(),
5195 _ => None,
5196 }
5197 }
5198
5199 /// Closing `)`-terminated argument list. v7.12.4 commonly
5200 /// sees the empty `()`; typed args round-trip but the
5201 /// executor (yet) doesn't invoke them.
5202 /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5203 /// it away, which is what PG does with one on a function parameter.
5204 fn skip_type_modifier(&mut self) {
5205 if !matches!(self.peek(), Token::LParen) {
5206 return;
5207 }
5208 // Only a numeric modifier — anything else is not one, and eating
5209 // it would swallow real grammar.
5210 let mut i = self.pos + 1;
5211 let mut seen_number = false;
5212 loop {
5213 match self.tokens.get(i) {
5214 Some(Token::Integer(_)) => seen_number = true,
5215 Some(Token::Comma) => {}
5216 Some(Token::RParen) => break,
5217 _ => return,
5218 }
5219 i += 1;
5220 }
5221 if !seen_number {
5222 return;
5223 }
5224 while self.pos <= i {
5225 self.advance();
5226 }
5227 }
5228
5229 fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5230 let mut args: Vec<FunctionArg> = Vec::new();
5231 if matches!(self.peek(), Token::RParen) {
5232 self.advance();
5233 return Ok(args);
5234 }
5235 loop {
5236 // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5237 // a reserved token; OUT / INOUT are bare idents.
5238 let mode = if matches!(self.peek(), Token::In) {
5239 self.advance();
5240 FunctionArgMode::In
5241 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5242 {
5243 self.advance();
5244 FunctionArgMode::Out
5245 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5246 {
5247 self.advance();
5248 FunctionArgMode::InOut
5249 } else {
5250 FunctionArgMode::In
5251 };
5252 // Optional name. The next token is either a name
5253 // (followed by a type ident) or the type itself.
5254 // Disambiguate by peeking ahead: if the token after
5255 // the next ident is also an ident, we treat the
5256 // first as the name.
5257 // v7.39 (round 315, V19) — take EVERY ident-like word up to
5258 // the comma or paren, then decide. Reading at most two of
5259 // them could not spell `x double precision` at all, and
5260 // silently mis-read the bare `double precision` as a
5261 // parameter named "double" — which is what made the same
5262 // signature key two different ways.
5263 let (name, ty_token) = {
5264 let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5265 while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5266 words.push(self.expect_ident_like()?);
5267 }
5268 // v7.39 (round 344) — a length / precision modifier on the
5269 // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5270 // accepts it and DROPS it — `pg_get_function_arguments`
5271 // reports plain `character varying` / `numeric`, measured on
5272 // 18.4 — but SPG raised `syntax error at or near "("`,
5273 // because the modifier's parens were never consumed.
5274 self.skip_type_modifier();
5275 // r1049 — `f(v bigint[])`. The array suffix parsed in
5276 // the column position, the cast position and (r1038)
5277 // the RETURNS position, but not here: the fifth
5278 // member of the same family, reported by sentori as
5279 // presumably the same code. It is now.
5280 let array_suffix = self.consume_array_suffix();
5281 let whole = words.join(" ");
5282 let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5283 {
5284 (Some(words[0].clone()), words[1..].join(" "))
5285 } else {
5286 (None, whole)
5287 };
5288 ty_token.push_str(&array_suffix);
5289 (name, ty_token)
5290 };
5291 // Type — try to map to ColumnTypeName, else Raw.
5292 let ty = match map_type_ident_to_column_type_name(&ty_token) {
5293 Some(t) => FunctionArgType::Typed(t),
5294 None => FunctionArgType::Raw(ty_token),
5295 };
5296 args.push(FunctionArg { mode, name, ty });
5297 match self.peek() {
5298 Token::Comma => {
5299 self.advance();
5300 continue;
5301 }
5302 Token::RParen => {
5303 self.advance();
5304 return Ok(args);
5305 }
5306 other => {
5307 return Err(self.err(alloc::format!(
5308 "expected , or ) in function arg list, got {other:?}"
5309 )));
5310 }
5311 }
5312 }
5313 }
5314
5315 fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5316 // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5317 // function whose row shape is named inline.
5318 if matches!(self.peek(), Token::Table)
5319 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5320 {
5321 self.advance(); // TABLE
5322 self.advance(); // (
5323 let mut cols: Vec<String> = Vec::new();
5324 loop {
5325 let cname = self.expect_ident_like()?;
5326 let mut ty: Vec<String> = Vec::new();
5327 loop {
5328 match self.peek() {
5329 Token::Comma | Token::RParen | Token::Eof => break,
5330 _ => {}
5331 }
5332 match self.advance() {
5333 Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5334 other => {
5335 if let Some(w) = unreserved_keyword_text(&other) {
5336 ty.push(w);
5337 }
5338 }
5339 }
5340 }
5341 cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5342 if matches!(self.peek(), Token::Comma) {
5343 self.advance();
5344 } else {
5345 break;
5346 }
5347 }
5348 if matches!(self.peek(), Token::RParen) {
5349 self.advance();
5350 }
5351 return Ok(FunctionReturn::Other(alloc::format!(
5352 "TABLE({})",
5353 cols.join(", ")
5354 )));
5355 }
5356 let ident = self.expect_ident_like()?;
5357 // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5358 if ident.eq_ignore_ascii_case("setof") {
5359 let inner = self.expect_ident_like()?;
5360 let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5361 return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5362 }
5363 if ident.eq_ignore_ascii_case("trigger") {
5364 return Ok(FunctionReturn::Trigger);
5365 }
5366 if ident.eq_ignore_ascii_case("void") {
5367 return Ok(FunctionReturn::Void);
5368 }
5369 // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5370 // RETURN position did not, so the `[` was a syntax error and the
5371 // whole migration stopped. sentori worked around it by returning
5372 // zero-padded text.
5373 let suffix = self.consume_array_suffix();
5374 if !suffix.is_empty() {
5375 return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5376 }
5377 match map_type_ident_to_column_type_name(&ident) {
5378 Some(t) => Ok(FunctionReturn::Type(t)),
5379 None => Ok(FunctionReturn::Other(ident)),
5380 }
5381 }
5382
5383 /// Consume any `[]` / `[N]` array markers after a type name and give
5384 /// back their text. Empty when there are none.
5385 fn consume_array_suffix(&mut self) -> String {
5386 let mut out = String::new();
5387 while matches!(self.peek(), Token::LBracket) {
5388 self.advance();
5389 // `[N]` is accepted and, as in PG, the length is not enforced.
5390 if let Token::Integer(n) = self.peek().clone() {
5391 self.advance();
5392 out.push_str(&alloc::format!("[{n}]"));
5393 } else {
5394 out.push_str("[]");
5395 }
5396 if matches!(self.peek(), Token::RBracket) {
5397 self.advance();
5398 }
5399 }
5400 out
5401 }
5402
5403 fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5404 match self.peek() {
5405 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5406 self.advance();
5407 let lang = self.expect_ident_like()?;
5408 Ok(Some(lang.to_ascii_lowercase()))
5409 }
5410 _ => Ok(None),
5411 }
5412 }
5413
5414 /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5415 /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5416 /// (expr)]*`. The `DOMAIN` keyword has already been
5417 /// consumed. PG allows the trailing constraints in any
5418 /// order; we approximate with a small loop.
5419 fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5420 let name = self.expect_ident_like()?;
5421 // Optional `AS`.
5422 if matches!(self.peek(), Token::As) {
5423 self.advance();
5424 }
5425 // v7.39 (round 259) — keep the raw type NAME when the base is not
5426 // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5427 // parent domain.
5428 let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _) =
5429 self.parse_type_with_implied_flags()?;
5430 let mut default: Option<Expr> = None;
5431 let mut not_null = false;
5432 let mut checks: Vec<Expr> = Vec::new();
5433 loop {
5434 match self.peek() {
5435 Token::Default => {
5436 if default.is_some() {
5437 return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5438 }
5439 self.advance();
5440 default = Some(self.parse_expr(0)?);
5441 }
5442 Token::Not => {
5443 self.advance();
5444 if !matches!(self.peek(), Token::Null) {
5445 return Err(self.err(alloc::format!(
5446 "expected NULL after NOT in DOMAIN, got {:?}",
5447 self.peek()
5448 )));
5449 }
5450 self.advance();
5451 not_null = true;
5452 }
5453 Token::Null => {
5454 self.advance();
5455 // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5456 // is the default-nullable marker (PG accepts it),
5457 // but AFTER a NOT NULL it is a conflict PG refuses
5458 // (`conflicting NULL/NOT NULL constraints`,
5459 // PG18-measured); the old arm no-opped both ways.
5460 if not_null {
5461 return Err(self.err(alloc::string::String::from(
5462 "conflicting NULL/NOT NULL constraints",
5463 )));
5464 }
5465 }
5466 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5467 self.advance();
5468 if !matches!(self.peek(), Token::LParen) {
5469 return Err(self.err(alloc::format!(
5470 "expected '(' after CHECK in DOMAIN, got {:?}",
5471 self.peek()
5472 )));
5473 }
5474 self.advance();
5475 let expr = self.parse_expr(0)?;
5476 if !matches!(self.peek(), Token::RParen) {
5477 return Err(self.err(alloc::format!(
5478 "expected ')' after CHECK expr, got {:?}",
5479 self.peek()
5480 )));
5481 }
5482 self.advance();
5483 checks.push(expr);
5484 }
5485 // CONSTRAINT <name> CHECK (…) — PG accepts a name
5486 // prefix on the constraint; we drop the name and
5487 // recurse into the constraint parsing.
5488 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5489 self.advance();
5490 let _ = self.expect_ident_like()?;
5491 }
5492 _ => break,
5493 }
5494 }
5495 Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5496 name,
5497 base_type,
5498 base_domain: base_user_ref,
5499 default,
5500 not_null,
5501 checks,
5502 }))
5503 }
5504
5505 /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5506 /// ('a', 'b', …)`. The `TYPE` keyword has already been
5507 /// consumed.
5508 fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5509 let name = self.expect_ident_like()?;
5510 // Required `AS`.
5511 if !matches!(self.peek(), Token::As) {
5512 return Err(self.err(alloc::format!(
5513 "expected AS after CREATE TYPE {name:?}, got {:?}",
5514 self.peek()
5515 )));
5516 }
5517 self.advance();
5518 // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5519 // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5520 // on the next token: `(` = composite, ident `ENUM` = enum.
5521 if matches!(self.peek(), Token::LParen) {
5522 self.advance();
5523 let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5524 let mut field_user_types: Vec<Option<String>> = Vec::new();
5525 // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5526 // is legal PG (an attribute-less composite; measured — the old
5527 // e2e note claimed PG requires at least one attribute).
5528 if matches!(self.peek(), Token::RParen) {
5529 self.advance();
5530 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5531 name,
5532 kind: crate::ast::TypeKind::Composite {
5533 fields,
5534 field_user_types,
5535 },
5536 }));
5537 }
5538 loop {
5539 let field_name = self.expect_ident_like()?;
5540 // v7.39 (round 264) — keep the raw type name when it is not
5541 // a builtin: that is how a NESTED composite field records
5542 // which composite it holds.
5543 let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _) =
5544 self.parse_type_with_implied_flags()?;
5545 fields.push((field_name, field_type));
5546 field_user_types.push(field_user_ref);
5547 if matches!(self.peek(), Token::Comma) {
5548 self.advance();
5549 continue;
5550 }
5551 if matches!(self.peek(), Token::RParen) {
5552 self.advance();
5553 break;
5554 }
5555 return Err(self.err(alloc::format!(
5556 "expected , or ) in composite field list, got {:?}",
5557 self.peek()
5558 )));
5559 }
5560 if fields.is_empty() {
5561 return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5562 }
5563 return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5564 name,
5565 kind: crate::ast::TypeKind::Composite {
5566 fields,
5567 field_user_types,
5568 },
5569 }));
5570 }
5571 // Required `ENUM` ident.
5572 let kind_ident = match self.peek().clone() {
5573 Token::Ident(s) | Token::QuotedIdent(s) => s,
5574 other => {
5575 return Err(self.err(alloc::format!(
5576 "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5577 )));
5578 }
5579 };
5580 if !kind_ident.eq_ignore_ascii_case("enum") {
5581 return Err(self.err(alloc::format!(
5582 "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5583 )));
5584 }
5585 self.advance();
5586 if !matches!(self.peek(), Token::LParen) {
5587 return Err(self.err(alloc::format!(
5588 "expected '(' after ENUM, got {:?}",
5589 self.peek()
5590 )));
5591 }
5592 self.advance();
5593 let mut labels: Vec<String> = Vec::new();
5594 loop {
5595 match self.peek().clone() {
5596 Token::String(s) => {
5597 self.advance();
5598 labels.push(s);
5599 }
5600 other => {
5601 return Err(
5602 self.err(alloc::format!("expected enum label string, got {other:?}"))
5603 );
5604 }
5605 }
5606 if matches!(self.peek(), Token::Comma) {
5607 self.advance();
5608 continue;
5609 }
5610 if matches!(self.peek(), Token::RParen) {
5611 self.advance();
5612 break;
5613 }
5614 return Err(self.err(alloc::format!(
5615 "expected , or ) in ENUM label list, got {:?}",
5616 self.peek()
5617 )));
5618 }
5619 if labels.is_empty() {
5620 return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5621 }
5622 Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5623 name,
5624 kind: crate::ast::TypeKind::Enum { labels },
5625 }))
5626 }
5627
5628 /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
5629 /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
5630 /// The `CREATE MATERIALIZED VIEW` keywords have already been
5631 /// consumed.
5632 fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
5633 let if_not_exists = self.parse_if_not_exists();
5634 let name = self.expect_ident_like()?;
5635 let mut columns: Vec<String> = Vec::new();
5636 if matches!(self.peek(), Token::LParen) {
5637 self.advance();
5638 loop {
5639 let c = self.expect_ident_like()?;
5640 columns.push(c);
5641 if matches!(self.peek(), Token::Comma) {
5642 self.advance();
5643 continue;
5644 }
5645 if matches!(self.peek(), Token::RParen) {
5646 self.advance();
5647 break;
5648 }
5649 return Err(self.err(alloc::format!(
5650 "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
5651 self.peek()
5652 )));
5653 }
5654 }
5655 if !matches!(self.peek(), Token::As) {
5656 return Err(self.err(alloc::format!(
5657 "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
5658 self.peek()
5659 )));
5660 }
5661 self.advance();
5662 // v7.39 (round 151) — a WITH-headed body is legal (read-only
5663 // CTEs only; the engine rejects data-modifying ones with PG's
5664 // message). A trailing `WITH [NO] DATA` can't START the body,
5665 // so WITH here heads the query.
5666 let body = if self.peek_is_with_kw() {
5667 self.advance();
5668 self.parse_nested_with_select()?
5669 } else {
5670 let body_stmt = self.parse_select_stmt()?;
5671 let Statement::Select(body) = body_stmt else {
5672 return Err(self.err(alloc::format!(
5673 "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
5674 )));
5675 };
5676 body
5677 };
5678 // Optional trailing `WITH [NO] DATA`.
5679 let with_data = self.parse_optional_with_data(true)?;
5680 Ok(Statement::CreateMaterializedView(
5681 crate::ast::CreateMaterializedViewStatement {
5682 temporary: false,
5683 name,
5684 if_not_exists,
5685 columns,
5686 body,
5687 with_data,
5688 as_plain_table: false,
5689 },
5690 ))
5691 }
5692
5693 /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
5694 /// `default_when_absent` is what to return if the tail is
5695 /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
5696 /// WITH DATA).
5697 fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
5698 let save = self.pos;
5699 // `WITH` is an Ident (not reserved in the lexer).
5700 let is_with = match self.peek() {
5701 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
5702 _ => false,
5703 };
5704 if !is_with {
5705 return Ok(default_when_absent);
5706 }
5707 self.advance();
5708 // Optional `NO`.
5709 let mut with_data = true;
5710 let is_no = match self.peek() {
5711 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
5712 _ => false,
5713 };
5714 if is_no {
5715 self.advance();
5716 with_data = false;
5717 }
5718 // Required `DATA` ident.
5719 let is_data = match self.peek() {
5720 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
5721 _ => false,
5722 };
5723 if is_data {
5724 self.advance();
5725 Ok(with_data)
5726 } else {
5727 // Caller's WITH wasn't WITH-DATA — rewind so the outer
5728 // parser can interpret it.
5729 self.pos = save;
5730 Ok(default_when_absent)
5731 }
5732 }
5733
5734 /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
5735 /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
5736 /// All keyword prefixes have already been consumed; the flags
5737 /// say which were present.
5738 fn parse_create_view_after_keyword(
5739 &mut self,
5740 or_replace: bool,
5741 _materialized_unused: bool,
5742 temporary: bool,
5743 ) -> Result<Statement, ParseError> {
5744 let if_not_exists = self.parse_if_not_exists();
5745 let name = self.expect_ident_like()?;
5746 // Optional `(col, col, …)` rename list.
5747 let mut columns: Vec<String> = Vec::new();
5748 if matches!(self.peek(), Token::LParen) {
5749 self.advance();
5750 loop {
5751 let c = self.expect_ident_like()?;
5752 columns.push(c);
5753 if matches!(self.peek(), Token::Comma) {
5754 self.advance();
5755 continue;
5756 }
5757 if matches!(self.peek(), Token::RParen) {
5758 self.advance();
5759 break;
5760 }
5761 return Err(self.err(alloc::format!(
5762 "expected , or ) in VIEW column list, got {:?}",
5763 self.peek()
5764 )));
5765 }
5766 }
5767 // Required `AS`.
5768 if !matches!(self.peek(), Token::As) {
5769 return Err(self.err(alloc::format!(
5770 "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
5771 self.peek()
5772 )));
5773 }
5774 self.advance();
5775 // Body: a regular SELECT statement. v7.39 (round 151) — a
5776 // WITH-headed body is legal too (read-only CTEs only; the
5777 // engine rejects data-modifying ones with PG's message).
5778 // Disambiguation vs `WITH CHECK OPTION`: a body can't START
5779 // with the check-option clause, so WITH here heads the query.
5780 let body = if self.peek_is_with_kw() {
5781 self.advance();
5782 self.parse_nested_with_select()?
5783 } else {
5784 let body_stmt = self.parse_select_stmt()?;
5785 let Statement::Select(body) = body_stmt else {
5786 return Err(self.err(alloc::format!(
5787 "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
5788 )));
5789 };
5790 body
5791 };
5792 // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
5793 // The SELECT parser stops before a trailing WITH, so it lands here.
5794 let check_option = if matches!(self.peek(),
5795 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
5796 {
5797 self.advance(); // WITH
5798 let opt = match self.peek() {
5799 Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
5800 self.advance();
5801 crate::ast::ViewCheckOption::Local
5802 }
5803 Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
5804 self.advance();
5805 crate::ast::ViewCheckOption::Cascaded
5806 }
5807 // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
5808 _ => crate::ast::ViewCheckOption::Cascaded,
5809 };
5810 if !matches!(self.peek(),
5811 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
5812 {
5813 return Err(self.err(alloc::format!(
5814 "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
5815 self.peek()
5816 )));
5817 }
5818 self.advance(); // CHECK
5819 if !matches!(self.peek(),
5820 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
5821 {
5822 return Err(self.err(alloc::format!(
5823 "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
5824 self.peek()
5825 )));
5826 }
5827 self.advance(); // OPTION
5828 Some(opt)
5829 } else {
5830 None
5831 };
5832 Ok(Statement::CreateView(crate::ast::CreateViewStatement {
5833 name,
5834 or_replace,
5835 if_not_exists,
5836 temporary,
5837 columns,
5838 body,
5839 check_option,
5840 }))
5841 }
5842
5843 /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
5844 /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
5845 /// consumed; `temporary` carries whether TEMPORARY was seen.
5846 fn parse_create_sequence_after_keyword(
5847 &mut self,
5848 temporary: bool,
5849 ) -> Result<Statement, ParseError> {
5850 let if_not_exists = self.parse_if_not_exists();
5851 let name = self.expect_ident_like()?;
5852 // Optional `AS data_type`.
5853 let data_type = if matches!(self.peek(), Token::As) {
5854 self.advance();
5855 Some(self.parse_sequence_data_type()?)
5856 } else {
5857 None
5858 };
5859 let options = self.parse_sequence_options(/* allow_restart = */ false)?;
5860 Ok(Statement::CreateSequence(
5861 crate::ast::CreateSequenceStatement {
5862 name,
5863 if_not_exists,
5864 temporary,
5865 data_type,
5866 options,
5867 },
5868 ))
5869 }
5870
5871 /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
5872 /// already been consumed; this is reached after `SEQUENCE`.
5873 /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
5874 fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5875 use crate::ast::AlterDomainAction as A;
5876 let name = self.expect_ident_like()?;
5877 // DROP / SET / ADD lex as reserved keyword tokens, not idents.
5878 let kw = match self.peek() {
5879 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
5880 Token::Drop => alloc::string::String::from("drop"),
5881 Token::Default => alloc::string::String::from("default"),
5882 other => {
5883 return Err(self.err(alloc::format!(
5884 "expected an ALTER DOMAIN action, got {other:?}"
5885 )));
5886 }
5887 };
5888 let action = match kw.as_str() {
5889 "add" => {
5890 self.advance();
5891 let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
5892 {
5893 self.advance();
5894 Some(self.expect_ident_like()?)
5895 } else {
5896 None
5897 };
5898 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
5899 return Err(self.err(alloc::format!(
5900 "ALTER DOMAIN ADD supports CHECK only, got {:?}",
5901 self.peek()
5902 )));
5903 }
5904 self.advance();
5905 if !matches!(self.peek(), Token::LParen) {
5906 return Err(self.err("expected '(' after CHECK".into()));
5907 }
5908 self.advance();
5909 let check = self.parse_expr(0)?;
5910 if !matches!(self.peek(), Token::RParen) {
5911 return Err(self.err("expected ')' after CHECK expression".into()));
5912 }
5913 self.advance();
5914 A::AddConstraint { name: cname, check }
5915 }
5916 "drop" => {
5917 self.advance();
5918 match self.peek() {
5919 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
5920 self.advance();
5921 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
5922 {
5923 self.advance();
5924 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
5925 {
5926 return Err(self.err("expected EXISTS after IF".into()));
5927 }
5928 self.advance();
5929 true
5930 } else {
5931 false
5932 };
5933 let cn = self.expect_ident_like()?;
5934 A::DropConstraint {
5935 name: cn,
5936 if_exists,
5937 }
5938 }
5939 Token::Default => {
5940 self.advance();
5941 A::DropDefault
5942 }
5943 Token::Not => {
5944 self.advance();
5945 if !matches!(self.peek(), Token::Null) {
5946 return Err(self.err("expected NULL after NOT".into()));
5947 }
5948 self.advance();
5949 A::DropNotNull
5950 }
5951 other => {
5952 return Err(self.err(alloc::format!(
5953 "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
5954 )));
5955 }
5956 }
5957 }
5958 "set" => {
5959 self.advance();
5960 match self.peek() {
5961 Token::Default => {
5962 self.advance();
5963 A::SetDefault(self.parse_expr(0)?)
5964 }
5965 Token::Not => {
5966 self.advance();
5967 if !matches!(self.peek(), Token::Null) {
5968 return Err(self.err("expected NULL after NOT".into()));
5969 }
5970 self.advance();
5971 A::SetNotNull
5972 }
5973 other => {
5974 return Err(self.err(alloc::format!(
5975 "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
5976 )));
5977 }
5978 }
5979 }
5980 "rename" => {
5981 self.advance();
5982 if !matches!(self.peek(), Token::To) {
5983 return Err(self.err("expected TO after RENAME".into()));
5984 }
5985 self.advance();
5986 A::RenameTo(self.expect_ident_like()?)
5987 }
5988 other => {
5989 return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
5990 }
5991 };
5992 Ok(Statement::AlterDomain { name, action })
5993 }
5994
5995 fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
5996 let if_exists = self.parse_if_exists();
5997 let name = self.expect_ident_like()?;
5998 // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
5999 // the option list (PG allows only one or the other).
6000 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6001 self.advance();
6002 if matches!(self.peek(), Token::To) {
6003 self.advance();
6004 } else {
6005 self.expect_keyword_ident("to")?;
6006 }
6007 let new = self.expect_ident_like()?;
6008 return Ok(Statement::AlterSequence(
6009 crate::ast::AlterSequenceStatement {
6010 name,
6011 if_exists,
6012 options: crate::ast::SequenceOptions::default(),
6013 rename_to: Some(new),
6014 },
6015 ));
6016 }
6017 let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6018 Ok(Statement::AlterSequence(
6019 crate::ast::AlterSequenceStatement {
6020 name,
6021 if_exists,
6022 options,
6023 rename_to: None,
6024 },
6025 ))
6026 }
6027
6028 fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6029 let kw = self.expect_ident_like()?;
6030 match kw.to_ascii_lowercase().as_str() {
6031 "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6032 "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6033 "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6034 other => Err(self.err(alloc::format!(
6035 "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6036 ))),
6037 }
6038 }
6039
6040 fn parse_sequence_options(
6041 &mut self,
6042 allow_restart: bool,
6043 ) -> Result<crate::ast::SequenceOptions, ParseError> {
6044 use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6045 let mut opts = SequenceOptions::default();
6046 #[allow(clippy::while_let_loop)]
6047 loop {
6048 // Match an ident; stop at any non-ident token (sentinel,
6049 // semicolon, end of statement).
6050 let kw_lc = match self.peek() {
6051 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6052 _ => break,
6053 };
6054 match kw_lc.as_str() {
6055 "increment" => {
6056 self.advance();
6057 // Optional BY.
6058 if self.peek_is_by() {
6059 self.advance();
6060 }
6061 opts.increment = Some(self.expect_signed_int()?);
6062 }
6063 "minvalue" => {
6064 self.advance();
6065 opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6066 }
6067 "maxvalue" => {
6068 self.advance();
6069 opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6070 }
6071 "no" => {
6072 self.advance();
6073 let what = self.expect_ident_like()?;
6074 match what.to_ascii_lowercase().as_str() {
6075 "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6076 "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6077 "cycle" => opts.cycle = Some(false),
6078 other => {
6079 return Err(self.err(alloc::format!(
6080 "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6081 )));
6082 }
6083 }
6084 }
6085 "start" => {
6086 self.advance();
6087 // Optional WITH.
6088 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6089 if s.eq_ignore_ascii_case("with"))
6090 {
6091 self.advance();
6092 }
6093 opts.start = Some(self.expect_signed_int()?);
6094 }
6095 "restart" if allow_restart => {
6096 self.advance();
6097 // Optional WITH n; bare RESTART means restart at START.
6098 let mut with_val: Option<i64> = None;
6099 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6100 if s.eq_ignore_ascii_case("with"))
6101 {
6102 self.advance();
6103 with_val = Some(self.expect_signed_int()?);
6104 } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6105 with_val = Some(self.expect_signed_int()?);
6106 }
6107 opts.restart = Some(with_val);
6108 }
6109 "cache" => {
6110 self.advance();
6111 opts.cache = Some(self.expect_signed_int()?);
6112 }
6113 "cycle" => {
6114 self.advance();
6115 opts.cycle = Some(true);
6116 }
6117 "owned" => {
6118 self.advance();
6119 match self.peek() {
6120 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6121 self.advance();
6122 }
6123 other => {
6124 return Err(
6125 self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6126 );
6127 }
6128 }
6129 // OWNED BY {NONE | tab.col}. Read just one ident
6130 // (NOT expect_ident_like which would auto-strip
6131 // a schema prefix and consume the `.col` we need).
6132 let first = match self.advance() {
6133 Token::Ident(s) | Token::QuotedIdent(s) => s,
6134 other => {
6135 return Err(self.err(alloc::format!(
6136 "expected identifier or NONE after OWNED BY, got {other:?}"
6137 )));
6138 }
6139 };
6140 if first.eq_ignore_ascii_case("none") {
6141 opts.owned_by = Some(SequenceOwnedBy::None);
6142 } else if matches!(self.peek(), Token::Dot) {
6143 self.advance();
6144 let second = match self.advance() {
6145 Token::Ident(s) | Token::QuotedIdent(s) => s,
6146 other => {
6147 return Err(self.err(alloc::format!(
6148 "expected column name after OWNED BY {first}., got {other:?}"
6149 )));
6150 }
6151 };
6152 // v7.17 dump-compat fix — pg_dump emits
6153 // OWNED BY clauses as
6154 // `schema.table.column` (three segments).
6155 // If a third `.<ident>` follows, treat the
6156 // first ident as schema (drop it; SPG is
6157 // single-schema) and the middle / last
6158 // pair as table.column. Otherwise it's
6159 // the two-segment form table.column.
6160 if matches!(self.peek(), Token::Dot) {
6161 self.advance();
6162 let third = match self.advance() {
6163 Token::Ident(s) | Token::QuotedIdent(s) => s,
6164 other => {
6165 return Err(self.err(alloc::format!(
6166 "expected column name after OWNED BY {first}.{second}., got {other:?}"
6167 )));
6168 }
6169 };
6170 let _ = first; // schema prefix discarded
6171 opts.owned_by = Some(SequenceOwnedBy::Column {
6172 table: second,
6173 column: third,
6174 });
6175 } else {
6176 opts.owned_by = Some(SequenceOwnedBy::Column {
6177 table: first,
6178 column: second,
6179 });
6180 }
6181 } else {
6182 return Err(self.err(alloc::format!(
6183 "expected table.column or NONE after OWNED BY, got {first:?}"
6184 )));
6185 }
6186 }
6187 _ => break,
6188 }
6189 }
6190 Ok(opts)
6191 }
6192
6193 fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6194 let neg = if matches!(self.peek(), Token::Minus) {
6195 self.advance();
6196 true
6197 } else {
6198 false
6199 };
6200 match self.peek() {
6201 Token::Integer(n) => {
6202 let v = *n;
6203 self.advance();
6204 Ok(if neg { -v } else { v })
6205 }
6206 other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6207 }
6208 }
6209
6210 /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6211 /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6212 /// clause is fully accepted and discarded — SPG always runs
6213 /// constraint checks immediately (single-writer model). The
6214 /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6215 /// in either order (per the SQL spec they're independent),
6216 /// though pg_dump always emits them in the canonical
6217 /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6218 /// Stops at the first token that isn't part of the clause.
6219 fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6220 self.consume_deferrable_clauses_timed().map(|_| ())
6221 }
6222
6223 /// v7.39 (round 288) — the same scan, but reporting what it saw:
6224 /// `(deferrable, initially_deferred)`. The clauses were parsed and
6225 /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6226 /// NOT DEFERRABLE and a circular-FK migration could not load.
6227 fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6228 let mut deferrable = false;
6229 let mut initially_deferred = false;
6230 loop {
6231 // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6232 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6233 self.advance();
6234 deferrable = true;
6235 if self.consume_optional_initially_clause()? {
6236 initially_deferred = true;
6237 }
6238 continue;
6239 }
6240 // `NOT DEFERRABLE` — already worked pre-3.1.
6241 if matches!(self.peek(), Token::Not) {
6242 let look = self.tokens.get(self.pos + 1);
6243 if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6244 self.advance(); // NOT
6245 self.advance(); // DEFERRABLE
6246 deferrable = false;
6247 initially_deferred = false;
6248 let _ = self.consume_optional_initially_clause()?;
6249 continue;
6250 }
6251 break;
6252 }
6253 // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6254 // accepts this without a leading [NOT] DEFERRABLE
6255 // (the timing keyword alone). pg_dump occasionally
6256 // emits it on FK constraints that inherit timing.
6257 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6258 if self.consume_optional_initially_clause()? {
6259 initially_deferred = true;
6260 // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6261 deferrable = true;
6262 }
6263 continue;
6264 }
6265 break;
6266 }
6267 Ok((deferrable, initially_deferred))
6268 }
6269
6270 /// Helper for [`consume_optional_deferrable_clauses`]. When the
6271 /// next token is `INITIALLY`, consume it plus the required
6272 /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6273 /// Returns true when the timing seen was `DEFERRED`.
6274 fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6275 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6276 return Ok(false);
6277 }
6278 self.advance(); // INITIALLY
6279 match self.advance() {
6280 Token::Ident(s)
6281 if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6282 {
6283 Ok(s.eq_ignore_ascii_case("deferred"))
6284 }
6285 other => Err(self.err(alloc::format!(
6286 "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6287 ))),
6288 }
6289 }
6290
6291 /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6292 /// in its entirety so the parser returns Empty without
6293 /// touching the runtime. The CREATE+PROCEDURE keywords are
6294 /// already consumed; this swallows everything from the
6295 /// procedure name through the matching `END`, including
6296 /// nested `BEGIN`/`END` blocks, internal `;` terminators
6297 /// (DELIMITER `//` makes the script splitter forward the
6298 /// whole block as one statement), `@var` session-variable
6299 /// references, and the trailing terminator.
6300 ///
6301 /// Tracks nesting depth so:
6302 /// BEGIN
6303 /// IF cond THEN
6304 /// BEGIN ... END;
6305 /// END IF;
6306 /// END
6307 /// terminates at the outer END.
6308 fn consume_mysql_routine_body(&mut self) {
6309 // Outer skeleton: name, (...), optional clauses, BEGIN
6310 // <body> END [;]. Scan for the first BEGIN — anything
6311 // before it is signature decoration we don't care about.
6312 // Once inside BEGIN, count up on BEGIN, down on END.
6313 let mut depth: i32 = 0;
6314 let mut started = false;
6315 loop {
6316 match self.peek().clone() {
6317 Token::Begin => {
6318 self.advance();
6319 depth += 1;
6320 started = true;
6321 }
6322 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6323 self.advance();
6324 if started {
6325 depth -= 1;
6326 if depth <= 0 {
6327 // Optional trailing ident (`END IF`,
6328 // `END LOOP`, `END WHILE`, `END CASE`,
6329 // `END label_name`) — eat the next
6330 // ident if present so we don't
6331 // mistake `END IF;` for the outer
6332 // close.
6333 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6334 // If the next token is one of the
6335 // PL/SQL block-closer keywords,
6336 // the END belongs to an inner
6337 // block; bump depth back up.
6338 let is_inner_close = matches!(
6339 self.peek(),
6340 Token::Ident(s) | Token::QuotedIdent(s)
6341 if matches!(
6342 s.to_ascii_lowercase().as_str(),
6343 "if" | "loop" | "while" | "case" | "repeat"
6344 )
6345 );
6346 if is_inner_close {
6347 self.advance();
6348 depth += 1;
6349 continue;
6350 }
6351 }
6352 // Eat optional trailing `;`.
6353 if matches!(self.peek(), Token::Semicolon) {
6354 self.advance();
6355 }
6356 return;
6357 }
6358 }
6359 }
6360 Token::Eof => return,
6361 _ => {
6362 self.advance();
6363 }
6364 }
6365 }
6366 }
6367
6368 /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6369 /// that appear between `CREATE` and `VIEW` in mysqldump output:
6370 ///
6371 /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6372 /// * `DEFINER = <user>` (user may be a quoted string, a bare
6373 /// ident, or `ident @ ident-or-quoted-string` host form)
6374 /// * `SQL SECURITY {DEFINER|INVOKER}`
6375 ///
6376 /// Each clause may appear at most once but in any order.
6377 /// The hints are pure planner / permission metadata that
6378 /// SPG's view-rewrite engine handles uniformly; we accept
6379 /// and discard. Returns `Ok(())` once a non-clause token is
6380 /// peeked (the caller then checks for the `VIEW` keyword).
6381 fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6382 loop {
6383 match self.peek().clone() {
6384 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6385 self.advance(); // ALGORITHM
6386 // Optional `=`. MySQL spec requires it but be
6387 // generous.
6388 if matches!(self.peek(), Token::Eq) {
6389 self.advance();
6390 }
6391 // UNDEFINED / MERGE / TEMPTABLE — accept any
6392 // bare ident; unknown values still parse so
6393 // future MySQL versions don't break.
6394 if matches!(
6395 self.peek(),
6396 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6397 ) {
6398 self.advance();
6399 }
6400 }
6401 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6402 self.advance(); // DEFINER
6403 if matches!(self.peek(), Token::Eq) {
6404 self.advance();
6405 }
6406 // User: quoted string, ident, OR ident @ host
6407 // (host may itself be quoted or bare).
6408 match self.peek().clone() {
6409 Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6410 self.advance();
6411 // Optional `@host`.
6412 if matches!(self.peek(), Token::At) {
6413 self.advance();
6414 if matches!(
6415 self.peek(),
6416 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6417 ) {
6418 self.advance();
6419 }
6420 }
6421 }
6422 _ => {}
6423 }
6424 }
6425 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6426 // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6427 // when followed by SECURITY — the dispatcher must
6428 // not consume a bare `SQL` token (it's not a
6429 // legal CREATE prefix on its own).
6430 let save = self.pos;
6431 self.advance(); // SQL
6432 if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6433 if s2.eq_ignore_ascii_case("security"))
6434 {
6435 self.advance(); // SECURITY
6436 // DEFINER / INVOKER trailing ident.
6437 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6438 self.advance();
6439 }
6440 } else {
6441 // Not a SQL SECURITY clause — roll back and
6442 // bail; the caller will error out cleanly.
6443 self.pos = save;
6444 return Ok(());
6445 }
6446 }
6447 _ => return Ok(()),
6448 }
6449 }
6450 }
6451
6452 fn parse_if_not_exists(&mut self) -> bool {
6453 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6454 {
6455 let save = self.pos;
6456 self.advance();
6457 if matches!(self.peek(), Token::Not) {
6458 self.advance();
6459 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6460 {
6461 self.advance();
6462 return true;
6463 }
6464 }
6465 self.pos = save;
6466 }
6467 false
6468 }
6469
6470 fn parse_if_exists(&mut self) -> bool {
6471 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6472 {
6473 let save = self.pos;
6474 self.advance();
6475 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6476 {
6477 self.advance();
6478 return true;
6479 }
6480 self.pos = save;
6481 }
6482 false
6483 }
6484
6485 /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6486 /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6487 /// been consumed.
6488 fn parse_create_trigger_after_keyword(
6489 &mut self,
6490 or_replace: bool,
6491 ) -> Result<Statement, ParseError> {
6492 let name = self.expect_ident_like()?;
6493 let timing = {
6494 let ident = self.expect_ident_like()?;
6495 if ident.eq_ignore_ascii_case("before") {
6496 TriggerTiming::Before
6497 } else if ident.eq_ignore_ascii_case("after") {
6498 TriggerTiming::After
6499 } else if ident.eq_ignore_ascii_case("instead") {
6500 let next = self.expect_ident_like()?;
6501 if !next.eq_ignore_ascii_case("of") {
6502 return Err(self.err(alloc::format!(
6503 "expected OF after INSTEAD in trigger timing, got {next:?}"
6504 )));
6505 }
6506 TriggerTiming::InsteadOf
6507 } else {
6508 return Err(self.err(alloc::format!(
6509 "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6510 )));
6511 }
6512 };
6513 // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6514 // OR is a reserved keyword token (Token::Or), not an Ident.
6515 // v7.13.0 — after an UPDATE event we may optionally see
6516 // `OF col, col, …` (mailrs round-5 G7). Columns are
6517 // captured into `update_columns` once across the whole
6518 // events list; multiple `UPDATE OF` clauses are rejected.
6519 let mut events: Vec<TriggerEvent> = Vec::new();
6520 let mut update_columns: Vec<String> = Vec::new();
6521 let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6522 events.push(first_ev);
6523 if !first_cols.is_empty() {
6524 update_columns = first_cols;
6525 }
6526 while matches!(self.peek(), Token::Or) {
6527 self.advance();
6528 let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6529 events.push(ev);
6530 if !cols.is_empty() {
6531 if !update_columns.is_empty() {
6532 return Err(
6533 self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6534 );
6535 }
6536 update_columns = cols;
6537 }
6538 }
6539 // ON <table>
6540 let tok = self.peek();
6541 let Token::On = tok else {
6542 return Err(self.err(alloc::format!(
6543 "expected ON after trigger events, got {tok:?}"
6544 )));
6545 };
6546 self.advance();
6547 let table = self.expect_ident_like()?;
6548 // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6549 // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6550 // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6551 // the trigger as a plain AFTER trigger (correct for every non-deferred
6552 // use; deferral timing is not yet honoured).
6553 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6554 if s.eq_ignore_ascii_case("from"))
6555 {
6556 self.advance();
6557 let _reftable = self.expect_ident_like()?;
6558 }
6559 self.consume_optional_deferrable_clauses()?;
6560 // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6561 // keyword (Token::For); EACH / ROW / STATEMENT are bare
6562 // idents.
6563 if !matches!(self.peek(), Token::For) {
6564 return Err(self.err(alloc::format!(
6565 "expected FOR EACH ROW / STATEMENT, got {:?}",
6566 self.peek()
6567 )));
6568 }
6569 self.advance();
6570 let for_each = {
6571 let e = self.expect_ident_like()?;
6572 if !e.eq_ignore_ascii_case("each") {
6573 return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6574 }
6575 let unit = self.expect_ident_like()?;
6576 if unit.eq_ignore_ascii_case("row") {
6577 TriggerForEach::Row
6578 } else if unit.eq_ignore_ascii_case("statement") {
6579 TriggerForEach::Statement
6580 } else {
6581 return Err(self.err(alloc::format!(
6582 "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6583 )));
6584 }
6585 };
6586 // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6587 let when_condition = if matches!(self.peek(),
6588 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6589 {
6590 self.advance();
6591 Some(self.parse_paren_expr("WHEN")?)
6592 } else {
6593 None
6594 };
6595 // EXECUTE FUNCTION/PROCEDURE name(...)
6596 let exec = self.expect_ident_like()?;
6597 if !exec.eq_ignore_ascii_case("execute") {
6598 return Err(self.err(alloc::format!(
6599 "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6600 )));
6601 }
6602 let fn_or_proc = self.expect_ident_like()?;
6603 if !(fn_or_proc.eq_ignore_ascii_case("function")
6604 || fn_or_proc.eq_ignore_ascii_case("procedure"))
6605 {
6606 return Err(self.err(alloc::format!(
6607 "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6608 )));
6609 }
6610 let function = self.expect_ident_like()?;
6611 // Optional empty arg list `()`.
6612 if matches!(self.peek(), Token::LParen) {
6613 self.advance();
6614 if !matches!(self.peek(), Token::RParen) {
6615 return Err(self.err(alloc::format!(
6616 "v7.12.4 trigger function calls take no args; got {:?}",
6617 self.peek()
6618 )));
6619 }
6620 self.advance();
6621 }
6622 Ok(Statement::CreateTrigger(CreateTriggerStatement {
6623 name,
6624 or_replace,
6625 timing,
6626 events,
6627 table,
6628 for_each,
6629 function,
6630 update_columns,
6631 when_condition,
6632 }))
6633 }
6634
6635 /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
6636 /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
6637 fn parse_create_rule_after_keyword(
6638 &mut self,
6639 or_replace: bool,
6640 ) -> Result<Statement, ParseError> {
6641 let name = self.expect_ident_like()?;
6642 if !matches!(self.peek(), Token::As) {
6643 return Err(self.err(alloc::format!(
6644 "expected AS in CREATE RULE, got {:?}",
6645 self.peek()
6646 )));
6647 }
6648 self.advance();
6649 if !matches!(self.peek(), Token::On) {
6650 return Err(self.err(alloc::format!(
6651 "expected ON in CREATE RULE, got {:?}",
6652 self.peek()
6653 )));
6654 }
6655 self.advance();
6656 let event = self.parse_rule_event()?;
6657 if !matches!(self.peek(), Token::To)
6658 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
6659 {
6660 return Err(self.err(alloc::format!(
6661 "expected TO after rule event, got {:?}",
6662 self.peek()
6663 )));
6664 }
6665 self.advance();
6666 let table = self.expect_ident_like()?;
6667 // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
6668 let when_condition = if matches!(self.peek(), Token::Where) {
6669 self.advance();
6670 Some(self.parse_expr(0)?)
6671 } else {
6672 None
6673 };
6674 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
6675 {
6676 return Err(self.err(alloc::format!(
6677 "expected DO in CREATE RULE, got {:?}",
6678 self.peek()
6679 )));
6680 }
6681 self.advance();
6682 // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
6683 let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
6684 {
6685 self.advance();
6686 true
6687 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
6688 self.advance();
6689 false
6690 } else {
6691 false
6692 };
6693 // `NOTHING` | `( cmd; … )` | `cmd`.
6694 let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
6695 {
6696 self.advance();
6697 Vec::new()
6698 } else if matches!(self.peek(), Token::LParen) {
6699 self.advance();
6700 let mut cmds = Vec::new();
6701 loop {
6702 cmds.push(self.parse_one_statement()?);
6703 if matches!(self.peek(), Token::Semicolon) {
6704 self.advance();
6705 if matches!(self.peek(), Token::RParen) {
6706 break;
6707 }
6708 continue;
6709 }
6710 break;
6711 }
6712 if !matches!(self.peek(), Token::RParen) {
6713 return Err(self.err(alloc::format!(
6714 "expected ) closing the CREATE RULE command list, got {:?}",
6715 self.peek()
6716 )));
6717 }
6718 self.advance();
6719 cmds
6720 } else {
6721 alloc::vec![self.parse_one_statement()?]
6722 };
6723 Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
6724 name,
6725 or_replace,
6726 event,
6727 table,
6728 instead,
6729 when_condition,
6730 commands,
6731 }))
6732 }
6733
6734 /// v7.39 (round 139) — a rule event keyword → uppercase string.
6735 fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
6736 if matches!(self.peek(), Token::Insert) {
6737 self.advance();
6738 return Ok(alloc::string::String::from("INSERT"));
6739 }
6740 if matches!(self.peek(), Token::Select) {
6741 self.advance();
6742 return Ok(alloc::string::String::from("SELECT"));
6743 }
6744 match self.peek() {
6745 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
6746 self.advance();
6747 Ok(alloc::string::String::from("UPDATE"))
6748 }
6749 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
6750 self.advance();
6751 Ok(alloc::string::String::from("DELETE"))
6752 }
6753 other => Err(self.err(alloc::format!(
6754 "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
6755 ))),
6756 }
6757 }
6758
6759 /// v7.13.0 — parse one trigger event, then optionally consume
6760 /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
6761 /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
6762 fn parse_trigger_event_with_optional_of(
6763 &mut self,
6764 ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
6765 let ev = self.parse_trigger_event()?;
6766 if !matches!(ev, TriggerEvent::Update) {
6767 return Ok((ev, Vec::new()));
6768 }
6769 // `OF` is a bare ident.
6770 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
6771 return Ok((ev, Vec::new()));
6772 }
6773 self.advance(); // OF
6774 let mut cols: Vec<String> = Vec::new();
6775 loop {
6776 cols.push(self.expect_ident_like()?);
6777 if matches!(self.peek(), Token::Comma) {
6778 self.advance();
6779 continue;
6780 }
6781 break;
6782 }
6783 if cols.is_empty() {
6784 return Err(
6785 self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
6786 );
6787 }
6788 Ok((ev, cols))
6789 }
6790
6791 /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
6792 /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
6793 /// before `BEGIN`, and IF / RAISE / embedded SQL statements
6794 /// inside the body.
6795 /// Called by [`parse_plpgsql_body`] after the body's tokens
6796 /// have been lexed into this temporary parser.
6797 pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
6798 // v7.12.6 — optional DECLARE prelude.
6799 let declarations = if matches!(
6800 self.peek(),
6801 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
6802 ) {
6803 self.advance();
6804 self.parse_plpgsql_declare_block()?
6805 } else {
6806 Vec::new()
6807 };
6808 // BEGIN keyword (PL/pgSQL — distinct from the SQL
6809 // `BEGIN` transaction-start, but we can reuse the
6810 // reserved Token::Begin since the body is a separate
6811 // lex/parse context).
6812 if !matches!(self.peek(), Token::Begin) {
6813 return Err(self.err(alloc::format!(
6814 "expected BEGIN at start of plpgsql block, got {:?}",
6815 self.peek()
6816 )));
6817 }
6818 self.advance();
6819 let statements = self.parse_plpgsql_stmt_list_until_end()?;
6820 // v7.37.20 (20.10) — optional EXCEPTION clause between the
6821 // body's last statement and the trailing END. When present
6822 // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
6823 // arms terminated by END.
6824 let exception_handlers = if matches!(
6825 self.peek(),
6826 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
6827 ) {
6828 self.advance();
6829 self.parse_plpgsql_exception_handlers()?
6830 } else {
6831 Vec::new()
6832 };
6833 Ok(PlPgSqlBlock {
6834 declarations,
6835 statements,
6836 exception_handlers,
6837 })
6838 }
6839
6840 /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
6841 /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
6842 fn parse_plpgsql_exception_handlers(
6843 &mut self,
6844 ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
6845 let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
6846 loop {
6847 // Stop at END — the block-level trailing END LOOP / END;
6848 // is handled by the caller.
6849 if matches!(
6850 self.peek(),
6851 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
6852 ) {
6853 return Ok(out);
6854 }
6855 // WHEN <cond> [OR <cond>]* THEN <body>
6856 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6857 {
6858 return Err(self.err(alloc::format!(
6859 "expected WHEN or END inside EXCEPTION clause, got {:?}",
6860 self.peek()
6861 )));
6862 }
6863 self.advance();
6864 let mut conditions: Vec<String> = Vec::new();
6865 conditions.push(self.expect_ident_like()?);
6866 while matches!(self.peek(), Token::Or) {
6867 self.advance();
6868 conditions.push(self.expect_ident_like()?);
6869 }
6870 let then_kw = self.expect_ident_like()?;
6871 if !then_kw.eq_ignore_ascii_case("then") {
6872 return Err(self.err(alloc::format!(
6873 "expected THEN after WHEN condition list, got {then_kw:?}"
6874 )));
6875 }
6876 let body = self.parse_plpgsql_stmt_list_until_end()?;
6877 out.push(crate::ast::ExceptionHandler { conditions, body });
6878 }
6879 }
6880
6881 /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
6882 /// prelude. Caller has already consumed `DECLARE`. We stop
6883 /// reading entries when we hit `BEGIN`.
6884 fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
6885 let mut out: Vec<PlPgSqlDeclare> = Vec::new();
6886 loop {
6887 if matches!(self.peek(), Token::Begin) {
6888 return Ok(out);
6889 }
6890 let name = self.expect_ident_like()?;
6891 // v7.37.20 (20.7) — type inference: if the next token is
6892 // `:=` or `=` (no explicit type), infer from the default
6893 // expression. Otherwise the ident that follows is the
6894 // declared type.
6895 //
6896 // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
6897 // (PG-standard). SPG parse-accepts and treats identically
6898 // to inference — the eventual runtime value determines
6899 // the local's type, which is faithful to how SPG handles
6900 // untyped locals today (see 20.7). Full compile-time
6901 // catalog lookup queues with v7.40 PL/pgSQL epic.
6902 let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
6903 // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
6904 // downstream declaration walker to type the local by
6905 // the runtime type of the default expression.
6906 FunctionArgType::Raw("_infer_".into())
6907 } else {
6908 let ty_token = self.expect_ident_like()?;
6909 // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
6910 // consume optional `.<ident>` qualifier + `%<KW>`
6911 // suffix. Both qualifier and suffix map to _infer_.
6912 if matches!(self.peek(), Token::Dot) {
6913 self.advance();
6914 let _ = self.expect_ident_like()?;
6915 }
6916 if matches!(self.peek(), Token::Percent) {
6917 self.advance();
6918 // Consume the trailing TYPE / ROWTYPE ident.
6919 let _ = self.expect_ident_like()?;
6920 FunctionArgType::Raw("_infer_".into())
6921 } else {
6922 match map_type_ident_to_column_type_name(&ty_token) {
6923 Some(t) => FunctionArgType::Typed(t),
6924 None => FunctionArgType::Raw(ty_token),
6925 }
6926 }
6927 };
6928 let default = match self.peek() {
6929 Token::ColonEq => {
6930 self.advance();
6931 Some(self.parse_expr(0)?)
6932 }
6933 Token::Eq => {
6934 // PL/pgSQL also accepts `=` for the
6935 // DECLARE default (PG treats them the same
6936 // in this position).
6937 self.advance();
6938 Some(self.parse_expr(0)?)
6939 }
6940 _ => None,
6941 };
6942 // Mandatory `;` between declarations.
6943 if !matches!(self.peek(), Token::Semicolon) {
6944 return Err(self.err(alloc::format!(
6945 "expected ; after DECLARE entry for {name:?}, got {:?}",
6946 self.peek()
6947 )));
6948 }
6949 self.advance();
6950 out.push(PlPgSqlDeclare { name, ty, default });
6951 }
6952 }
6953
6954 /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
6955 /// the terminating `END;` (or `END IF;` etc — handled by the
6956 /// per-construct sub-parsers). Used by both the outer block
6957 /// and the IF/ELSE branch bodies.
6958 fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
6959 let mut statements: Vec<PlPgSqlStmt> = Vec::new();
6960 loop {
6961 // Allow trailing semicolons + END.
6962 while matches!(self.peek(), Token::Semicolon) {
6963 self.advance();
6964 }
6965 // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
6966 if matches!(
6967 self.peek(),
6968 Token::Ident(s) | Token::QuotedIdent(s)
6969 if s.eq_ignore_ascii_case("end")
6970 || s.eq_ignore_ascii_case("else")
6971 || s.eq_ignore_ascii_case("elsif")
6972 || s.eq_ignore_ascii_case("elseif")
6973 || s.eq_ignore_ascii_case("exception")
6974 || s.eq_ignore_ascii_case("when")
6975 ) {
6976 return Ok(statements);
6977 }
6978 // Otherwise: one statement, then expect `;` or
6979 // a block-terminator keyword.
6980 let stmt = self.parse_plpgsql_stmt()?;
6981 statements.push(stmt);
6982 match self.peek() {
6983 Token::Semicolon => {
6984 self.advance();
6985 }
6986 Token::Ident(s) | Token::QuotedIdent(s)
6987 if s.eq_ignore_ascii_case("end")
6988 || s.eq_ignore_ascii_case("else")
6989 || s.eq_ignore_ascii_case("elsif")
6990 || s.eq_ignore_ascii_case("elseif")
6991 || s.eq_ignore_ascii_case("exception")
6992 || s.eq_ignore_ascii_case("when") =>
6993 {
6994 // Final statement of the block without `;`.
6995 }
6996 other => {
6997 return Err(self.err(alloc::format!(
6998 "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
6999 )));
7000 }
7001 }
7002 }
7003 }
7004
7005 fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7006 // RETURN keyword?
7007 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7008 {
7009 self.advance();
7010 return self.parse_plpgsql_return();
7011 }
7012 // v7.12.6 — IF block.
7013 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7014 {
7015 self.advance();
7016 return self.parse_plpgsql_if();
7017 }
7018 // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7019 // Detected by peeking that token pos+3 is Ident("execute").
7020 if matches!(self.peek(), Token::For)
7021 && matches!(
7022 self.tokens.get(self.pos + 1),
7023 Some(Token::Ident(_) | Token::QuotedIdent(_))
7024 )
7025 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7026 && matches!(
7027 self.tokens.get(self.pos + 3),
7028 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7029 )
7030 {
7031 self.advance(); // FOR
7032 let var = self.expect_ident_like()?;
7033 self.advance(); // IN
7034 self.advance(); // EXECUTE
7035 // Prescan for LOOP at paren depth 0 so parse_expr stops
7036 // before the LOOP keyword (same trick as the bare-SELECT
7037 // ForQuery arm).
7038 let mut depth: i32 = 0;
7039 let mut loop_pos: Option<usize> = None;
7040 let mut scan = self.pos;
7041 while scan < self.tokens.len() {
7042 match self.tokens.get(scan) {
7043 Some(Token::LParen) => depth += 1,
7044 Some(Token::RParen) => depth -= 1,
7045 Some(Token::Ident(s) | Token::QuotedIdent(s))
7046 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7047 {
7048 loop_pos = Some(scan);
7049 break;
7050 }
7051 _ => {}
7052 }
7053 scan += 1;
7054 }
7055 let loop_pos = loop_pos.ok_or_else(|| {
7056 self.err(alloc::format!(
7057 "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7058 ))
7059 })?;
7060 let saved_loop = self.tokens[loop_pos].clone();
7061 self.tokens[loop_pos] = Token::Semicolon;
7062 let expr_result = self.parse_expr(0);
7063 self.tokens[loop_pos] = saved_loop;
7064 let sql_expr = expr_result?;
7065 let loop_kw = self.expect_ident_like()?;
7066 if !loop_kw.eq_ignore_ascii_case("loop") {
7067 return Err(self.err(alloc::format!(
7068 "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7069 )));
7070 }
7071 let body = self.parse_plpgsql_stmt_list_until_end()?;
7072 let end_kw = self.expect_ident_like()?;
7073 if !end_kw.eq_ignore_ascii_case("end") {
7074 return Err(self.err(alloc::format!(
7075 "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7076 )));
7077 }
7078 let loop_kw2 = self.expect_ident_like()?;
7079 if !loop_kw2.eq_ignore_ascii_case("loop") {
7080 return Err(self.err(alloc::format!(
7081 "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7082 )));
7083 }
7084 return Ok(PlPgSqlStmt::ForExecute {
7085 var,
7086 sql_expr,
7087 body,
7088 });
7089 }
7090 // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7091 //
7092 // Two syntactic forms:
7093 // FOR var IN SELECT ... ORDER BY ... LOOP ...
7094 // FOR var IN (SELECT ...) LOOP ...
7095 //
7096 // Bare-SELECT form: to keep parse_select_stmt from swallowing
7097 // the trailing `LOOP` keyword as a table alias, we prescan
7098 // forward to find LOOP at paren depth 0, splice a fake
7099 // Semicolon at that position (so SELECT parses cleanly),
7100 // then re-splice LOOP back in.
7101 //
7102 // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7103 // LOOP directly — no scan required.
7104 if matches!(self.peek(), Token::For)
7105 && matches!(
7106 self.tokens.get(self.pos + 1),
7107 Some(Token::Ident(_) | Token::QuotedIdent(_))
7108 )
7109 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7110 && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7111 || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7112 {
7113 self.advance(); // FOR
7114 let var = self.expect_ident_like()?;
7115 // IN
7116 self.advance();
7117 let query = if matches!(self.peek(), Token::LParen) {
7118 // Paren-wrapped SELECT.
7119 self.advance();
7120 let inner = self.parse_select_stmt()?;
7121 let Statement::Select(q) = inner else {
7122 return Err(self.err(alloc::format!(
7123 "expected SELECT inside (…), got {:?}",
7124 self.peek()
7125 )));
7126 };
7127 if !matches!(self.peek(), Token::RParen) {
7128 return Err(self.err(alloc::format!(
7129 "expected ')' after FOR-IN-SELECT body, got {:?}",
7130 self.peek()
7131 )));
7132 }
7133 self.advance();
7134 q
7135 } else {
7136 // Bare SELECT: prescan to find the LOOP boundary.
7137 let mut depth: i32 = 0;
7138 let mut loop_pos: Option<usize> = None;
7139 let mut scan = self.pos;
7140 while scan < self.tokens.len() {
7141 match self.tokens.get(scan) {
7142 Some(Token::LParen) => depth += 1,
7143 Some(Token::RParen) => depth -= 1,
7144 Some(Token::Ident(s) | Token::QuotedIdent(s))
7145 if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7146 {
7147 loop_pos = Some(scan);
7148 break;
7149 }
7150 _ => {}
7151 }
7152 scan += 1;
7153 }
7154 let loop_pos = loop_pos.ok_or_else(|| {
7155 self.err(alloc::format!(
7156 "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7157 ))
7158 })?;
7159 // Swap the LOOP token with a synthetic Semicolon so
7160 // parse_select_stmt stops there, then restore afterward.
7161 let saved_loop = self.tokens[loop_pos].clone();
7162 self.tokens[loop_pos] = Token::Semicolon;
7163 let parse_result = self.parse_select_stmt();
7164 self.tokens[loop_pos] = saved_loop;
7165 let inner = parse_result?;
7166 let Statement::Select(q) = inner else {
7167 return Err(self.err(alloc::format!(
7168 "expected SELECT after FOR <var> IN, got {:?}",
7169 self.peek()
7170 )));
7171 };
7172 q
7173 };
7174 let loop_kw = self.expect_ident_like()?;
7175 if !loop_kw.eq_ignore_ascii_case("loop") {
7176 return Err(self.err(alloc::format!(
7177 "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7178 )));
7179 }
7180 let body = self.parse_plpgsql_stmt_list_until_end()?;
7181 let end_kw = self.expect_ident_like()?;
7182 if !end_kw.eq_ignore_ascii_case("end") {
7183 return Err(self.err(alloc::format!(
7184 "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7185 )));
7186 }
7187 let loop_kw2 = self.expect_ident_like()?;
7188 if !loop_kw2.eq_ignore_ascii_case("loop") {
7189 return Err(self.err(alloc::format!(
7190 "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7191 )));
7192 }
7193 return Ok(PlPgSqlStmt::ForQuery {
7194 var,
7195 query: Box::new(query),
7196 body,
7197 });
7198 }
7199 // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7200 // FOR is a reserved keyword token (Token::For).
7201 if matches!(self.peek(), Token::For)
7202 && matches!(
7203 self.tokens.get(self.pos + 1),
7204 Some(Token::Ident(_) | Token::QuotedIdent(_))
7205 )
7206 && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7207 {
7208 self.advance(); // FOR
7209 let var = self.expect_ident_like()?;
7210 if !matches!(self.peek(), Token::In) {
7211 return Err(self.err(alloc::format!(
7212 "expected IN after FOR <var>, got {:?}",
7213 self.peek()
7214 )));
7215 }
7216 self.advance();
7217 let reverse = matches!(
7218 self.peek(),
7219 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7220 );
7221 if reverse {
7222 self.advance();
7223 }
7224 let start = self.parse_expr(0)?;
7225 if !matches!(self.peek(), Token::DotDot) {
7226 return Err(self.err(alloc::format!(
7227 "expected '..' between FOR loop bounds, got {:?}",
7228 self.peek()
7229 )));
7230 }
7231 self.advance();
7232 let end = self.parse_expr(0)?;
7233 let loop_kw = self.expect_ident_like()?;
7234 if !loop_kw.eq_ignore_ascii_case("loop") {
7235 return Err(self.err(alloc::format!(
7236 "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7237 )));
7238 }
7239 let body = self.parse_plpgsql_stmt_list_until_end()?;
7240 let end_kw = self.expect_ident_like()?;
7241 if !end_kw.eq_ignore_ascii_case("end") {
7242 return Err(self.err(alloc::format!(
7243 "expected END LOOP after FOR body, got {end_kw:?}"
7244 )));
7245 }
7246 let loop_kw2 = self.expect_ident_like()?;
7247 if !loop_kw2.eq_ignore_ascii_case("loop") {
7248 return Err(self.err(alloc::format!(
7249 "expected END LOOP after FOR body, got END {loop_kw2:?}"
7250 )));
7251 }
7252 return Ok(PlPgSqlStmt::ForRange {
7253 var,
7254 start,
7255 end,
7256 reverse,
7257 body,
7258 });
7259 }
7260 // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7261 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7262 {
7263 self.advance();
7264 let body = self.parse_plpgsql_stmt_list_until_end()?;
7265 let end_kw = self.expect_ident_like()?;
7266 if !end_kw.eq_ignore_ascii_case("end") {
7267 return Err(self.err(alloc::format!(
7268 "expected END LOOP after LOOP body, got {end_kw:?}"
7269 )));
7270 }
7271 let loop_kw = self.expect_ident_like()?;
7272 if !loop_kw.eq_ignore_ascii_case("loop") {
7273 return Err(self.err(alloc::format!(
7274 "expected END LOOP after LOOP body, got END {loop_kw:?}"
7275 )));
7276 }
7277 return Ok(PlPgSqlStmt::Loop { body });
7278 }
7279 // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7280 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7281 {
7282 self.advance();
7283 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7284 {
7285 self.advance();
7286 Some(self.parse_expr(0)?)
7287 } else {
7288 None
7289 };
7290 return Ok(PlPgSqlStmt::Exit { when });
7291 }
7292 // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7293 // already-parsed Statement or a runtime-computed SQL string.
7294 // The disambiguator vs the extended-query-protocol `EXECUTE
7295 // <stmt_name>` (which is a top-level Statement, not a
7296 // plpgsql line) is that inside a DO block / trigger body the
7297 // EXECUTE keyword ALWAYS refers to dynamic SQL.
7298 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7299 {
7300 self.advance();
7301 let sql = self.parse_expr(0)?;
7302 return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7303 }
7304 // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7305 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7306 {
7307 self.advance();
7308 let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7309 {
7310 self.advance();
7311 Some(self.parse_expr(0)?)
7312 } else {
7313 None
7314 };
7315 return Ok(PlPgSqlStmt::Continue { when });
7316 }
7317 // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7318 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7319 {
7320 self.advance();
7321 let condition = self.parse_expr(0)?;
7322 let loop_kw = self.expect_ident_like()?;
7323 if !loop_kw.eq_ignore_ascii_case("loop") {
7324 return Err(self.err(alloc::format!(
7325 "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7326 )));
7327 }
7328 let body = self.parse_plpgsql_stmt_list_until_end()?;
7329 // Expect END LOOP.
7330 let end_kw = self.expect_ident_like()?;
7331 if !end_kw.eq_ignore_ascii_case("end") {
7332 return Err(self.err(alloc::format!(
7333 "expected END LOOP after WHILE body, got {end_kw:?}"
7334 )));
7335 }
7336 let loop_kw2 = self.expect_ident_like()?;
7337 if !loop_kw2.eq_ignore_ascii_case("loop") {
7338 return Err(self.err(alloc::format!(
7339 "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7340 )));
7341 }
7342 return Ok(PlPgSqlStmt::While { condition, body });
7343 }
7344 // v7.12.6 — RAISE.
7345 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7346 {
7347 self.advance();
7348 return self.parse_plpgsql_raise();
7349 }
7350 // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7351 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7352 {
7353 self.advance();
7354 let condition = self.parse_expr(0)?;
7355 let message = if matches!(self.peek(), Token::Comma) {
7356 self.advance();
7357 Some(self.parse_expr(0)?)
7358 } else {
7359 None
7360 };
7361 return Ok(PlPgSqlStmt::Assert { condition, message });
7362 }
7363 // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7364 // "PERFORM is equivalent to SELECT but discards the
7365 // result." Side effects (function calls, RAISE inside
7366 // SQL functions, etc.) still execute. We desugar to
7367 // `SELECT <body>` and wrap in EmbeddedSql so the engine's
7368 // existing embedded-statement path handles execution +
7369 // result-discard cleanly. The result is naturally
7370 // discarded because EmbeddedSql doesn't propagate row
7371 // sets back to the plpgsql interpreter.
7372 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7373 {
7374 self.advance();
7375 // Splice a synthetic Token::Select into the stream at
7376 // the current position so parse_select_stmt parses the
7377 // remainder as a normal SELECT body. Token-stream
7378 // surgery mirrors the try_parse_plpgsql_select_into
7379 // pattern used for SELECT … INTO desugaring.
7380 self.tokens.insert(self.pos, Token::Select);
7381 let select = self.parse_select_stmt()?;
7382 let Statement::Select(s) = select else {
7383 return Err(self.err(alloc::format!(
7384 "expected SELECT body after PERFORM, got {:?}",
7385 self.peek()
7386 )));
7387 };
7388 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7389 }
7390 // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7391 // plpgsql-specific shape (mailrs round-10 migrate-042).
7392 // PG's SELECT INTO at top-level SQL would CREATE a new
7393 // table; inside plpgsql it ASSIGNS the query result to
7394 // a local variable. We detect the INTO at paren-depth
7395 // 0 between SELECT and the statement boundary; if
7396 // found, split the token stream into "pre-INTO
7397 // projection" + "var" + "post-INTO FROM/WHERE…" and
7398 // rebuild as a SelectInto with a regular SELECT body
7399 // (no INTO clause).
7400 if matches!(self.peek(), Token::Select)
7401 && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7402 {
7403 return Ok(PlPgSqlStmt::SelectInto {
7404 var: var_name,
7405 body: Box::new(select_body),
7406 });
7407 }
7408 // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7409 // SELECT can appear directly inside a trigger body; we
7410 // recurse into the regular Statement parser, which will
7411 // stop at the trailing `;` (which our caller then
7412 // consumes).
7413 // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7414 // also embed ALTER / CREATE / DROP statements; route
7415 // those through the same parser so the DO body parses
7416 // cleanly.
7417 if matches!(self.peek(), Token::Insert)
7418 || matches!(self.peek(), Token::Select)
7419 || matches!(self.peek(), Token::Create)
7420 || matches!(self.peek(), Token::Drop)
7421 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7422 if s.eq_ignore_ascii_case("update")
7423 || s.eq_ignore_ascii_case("delete")
7424 || s.eq_ignore_ascii_case("alter"))
7425 {
7426 let stmt = self.parse_one_statement()?;
7427 return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7428 }
7429 // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7430 // followed by `:=` and an expression.
7431 let target = self.parse_plpgsql_assign_target()?;
7432 // PL/pgSQL assignment uses `:=`. The lexer represents
7433 // this as a colon followed by `=`; check both shapes.
7434 match self.peek() {
7435 Token::ColonEq => {
7436 self.advance();
7437 }
7438 Token::Colon => {
7439 self.advance();
7440 if !matches!(self.peek(), Token::Eq) {
7441 return Err(self.err(alloc::format!(
7442 "expected := after plpgsql assign target, got `:` then {:?}",
7443 self.peek()
7444 )));
7445 }
7446 self.advance();
7447 }
7448 other => {
7449 return Err(self.err(alloc::format!(
7450 "expected := after plpgsql assign target, got {other:?}"
7451 )));
7452 }
7453 }
7454 let value = self.parse_expr(0)?;
7455 Ok(PlPgSqlStmt::Assign { target, value })
7456 }
7457
7458 /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7459 /// [ELSE body] END IF`. `IF` keyword already consumed.
7460 fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7461 let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7462 let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7463 loop {
7464 // <expr> THEN
7465 let cond = self.parse_expr(0)?;
7466 let then_kw = self.expect_ident_like()?;
7467 if !then_kw.eq_ignore_ascii_case("then") {
7468 return Err(self.err(alloc::format!(
7469 "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7470 )));
7471 }
7472 let body = self.parse_plpgsql_stmt_list_until_end()?;
7473 branches.push((cond, body));
7474 // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7475 match self.peek() {
7476 Token::Ident(s) | Token::QuotedIdent(s)
7477 if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7478 {
7479 self.advance();
7480 continue;
7481 }
7482 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7483 self.advance();
7484 else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7485 break;
7486 }
7487 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7488 break;
7489 }
7490 other => {
7491 return Err(self.err(alloc::format!(
7492 "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7493 )));
7494 }
7495 }
7496 }
7497 // Expect `END IF` (the END keyword is the one we're
7498 // looking at right now).
7499 let end_kw = self.expect_ident_like()?;
7500 if !end_kw.eq_ignore_ascii_case("end") {
7501 return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7502 }
7503 let if_kw = self.expect_ident_like()?;
7504 if !if_kw.eq_ignore_ascii_case("if") {
7505 return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7506 }
7507 Ok(PlPgSqlStmt::If {
7508 branches,
7509 else_branch,
7510 })
7511 }
7512
7513 /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7514 /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7515 /// is already consumed.
7516 fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7517 let lvl_ident = self.expect_ident_like()?;
7518 let level = match lvl_ident.to_ascii_lowercase().as_str() {
7519 "notice" => RaiseLevel::Notice,
7520 "warning" => RaiseLevel::Warning,
7521 "info" => RaiseLevel::Info,
7522 "log" => RaiseLevel::Log,
7523 "debug" => RaiseLevel::Debug,
7524 "exception" => RaiseLevel::Exception,
7525 other => {
7526 return Err(self.err(alloc::format!(
7527 "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7528 )));
7529 }
7530 };
7531 // Message: required for v7.12.6. PG accepts a bare
7532 // RAISE-rethrow form (no message), reserved for future
7533 // RAISE-no-args support.
7534 let Token::String(msg) = self.peek() else {
7535 return Err(self.err(alloc::format!(
7536 "expected RAISE message string, got {:?}",
7537 self.peek()
7538 )));
7539 };
7540 let message = msg.clone();
7541 self.advance();
7542 // Optional comma-separated args (PG `%` format substitution).
7543 let mut args: Vec<Expr> = Vec::new();
7544 while matches!(self.peek(), Token::Comma) {
7545 self.advance();
7546 args.push(self.parse_expr(0)?);
7547 }
7548 Ok(PlPgSqlStmt::Raise {
7549 level,
7550 message,
7551 args,
7552 })
7553 }
7554
7555 /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7556 /// <projection> INTO <var> [FROM …]` (mailrs round-10
7557 /// migrate-042). Returns `(rebuilt_select_without_into,
7558 /// var_name)` when the pattern matches; `None` for
7559 /// regular SELECTs (those go through the embedded-SQL
7560 /// path). Token-stream surgery so the rebuilt SELECT
7561 /// parses through the regular `parse_select_stmt`.
7562 #[allow(clippy::too_many_lines)]
7563 fn try_parse_plpgsql_select_into(
7564 &mut self,
7565 ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7566 // Scan forward from `self.pos + 1` (past Token::Select)
7567 // for Token::Into at paren-depth 0, stopping at the
7568 // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7569 // end the plpgsql statement.
7570 let start = self.pos;
7571 let mut into_pos: Option<usize> = None;
7572 let mut depth: i32 = 0;
7573 let mut i = start + 1;
7574 while i < self.tokens.len() {
7575 match &self.tokens[i] {
7576 Token::LParen => depth += 1,
7577 Token::RParen => depth -= 1,
7578 Token::Semicolon if depth == 0 => break,
7579 Token::Ident(s)
7580 if depth == 0
7581 && (s.eq_ignore_ascii_case("end")
7582 || s.eq_ignore_ascii_case("else")
7583 || s.eq_ignore_ascii_case("elsif")) =>
7584 {
7585 break;
7586 }
7587 Token::Into if depth == 0 => {
7588 into_pos = Some(i);
7589 break;
7590 }
7591 _ => {}
7592 }
7593 i += 1;
7594 }
7595 let Some(into_at) = into_pos else {
7596 return Ok(None);
7597 };
7598 // The token immediately after INTO must be the target
7599 // var ident; anything else (e.g. INSERT INTO table)
7600 // ruled out by the depth-0 check above. Capture it.
7601 let var = match self.tokens.get(into_at + 1) {
7602 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7603 other => {
7604 return Err(self.err(alloc::format!(
7605 "expected variable name after SELECT … INTO, got {other:?}"
7606 )));
7607 }
7608 };
7609 // Find the end of the plpgsql SELECT INTO statement —
7610 // same boundary rules as the depth-0 scan above.
7611 let mut end = into_at + 2;
7612 let mut depth2: i32 = 0;
7613 while end < self.tokens.len() {
7614 match &self.tokens[end] {
7615 Token::LParen => depth2 += 1,
7616 Token::RParen => depth2 -= 1,
7617 Token::Semicolon if depth2 == 0 => break,
7618 Token::Ident(s)
7619 if depth2 == 0
7620 && (s.eq_ignore_ascii_case("end")
7621 || s.eq_ignore_ascii_case("else")
7622 || s.eq_ignore_ascii_case("elsif")) =>
7623 {
7624 break;
7625 }
7626 _ => {}
7627 }
7628 end += 1;
7629 }
7630 // Rebuild a token stream that represents the SELECT
7631 // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
7632 // post-var tokens up to statement end]. Run the
7633 // regular `parse_select_stmt` against it.
7634 let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
7635 for j in start..into_at {
7636 rebuilt.push(self.tokens[j].clone());
7637 }
7638 for j in (into_at + 2)..end {
7639 rebuilt.push(self.tokens[j].clone());
7640 }
7641 rebuilt.push(Token::Eof);
7642 let saved_pos = self.pos;
7643 let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
7644 self.pos = 0;
7645 // parse_select_stmt → parse_bare_select consumes Token::Select itself.
7646 if !matches!(self.peek(), Token::Select) {
7647 self.tokens = saved_tokens;
7648 self.pos = saved_pos;
7649 return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
7650 }
7651 let sel = self.parse_select_stmt();
7652 self.tokens = saved_tokens;
7653 self.pos = end;
7654 let sel = sel?;
7655 let Statement::Select(body) = sel else {
7656 return Err(self.err(alloc::format!(
7657 "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
7658 )));
7659 };
7660 Ok(Some((body, var)))
7661 }
7662
7663 fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
7664 // v7.16.1 — read the head token DIRECTLY rather than
7665 // via `expect_ident_like`. The v7.14.0 schema-qualifier
7666 // strip (`public.t` → `t`) inside `expect_ident_like`
7667 // greedily consumes any `ident . ident` pair, which
7668 // silently turned every `NEW.col := …` /
7669 // `OLD.col := …` plpgsql assignment into a Local("col")
7670 // assignment — the head "new"/"old" was eaten as if it
7671 // were a schema name and the Dot was consumed too, so
7672 // this function's own `peek() == Token::Dot` check
7673 // below never fired. Every BEFORE trigger that rewrote
7674 // a NEW cell was a silent no-op for two major releases
7675 // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
7676 // gate failures were investigated as v7.16.1 backlog.
7677 let head = match self.advance() {
7678 Token::Ident(s) | Token::QuotedIdent(s) => s,
7679 other => {
7680 return Err(self.err(alloc::format!(
7681 "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
7682 )));
7683 }
7684 };
7685 if matches!(self.peek(), Token::Dot) {
7686 self.advance();
7687 let col = self.expect_ident_like()?;
7688 if head.eq_ignore_ascii_case("new") {
7689 return Ok(AssignTarget::NewColumn(col));
7690 }
7691 if head.eq_ignore_ascii_case("old") {
7692 return Ok(AssignTarget::OldColumn(col));
7693 }
7694 return Err(self.err(alloc::format!(
7695 "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
7696 got {head:?}.<col>"
7697 )));
7698 }
7699 Ok(AssignTarget::Local(head))
7700 }
7701
7702 fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7703 // RETURN NEW / OLD / NULL — bare-ident forms.
7704 match self.peek() {
7705 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
7706 self.advance();
7707 return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
7708 }
7709 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
7710 self.advance();
7711 return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
7712 }
7713 Token::Null => {
7714 self.advance();
7715 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7716 }
7717 // Bare `RETURN;` (no value) — treated as `RETURN NULL`
7718 // per PL/pgSQL convention.
7719 Token::Semicolon => {
7720 return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
7721 }
7722 _ => {}
7723 }
7724 // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
7725 // EXECUTE <expr>. In a DO block context RETURN QUERY has no
7726 // caller-visible effect (blocks don't return sets), so we
7727 // desugar it identically to PERFORM: parse the SELECT (or
7728 // EXECUTE dynamic) as embedded SQL that runs for side
7729 // effects and discards the result. RETURN NEXT <expr>
7730 // (single-row accumulator) queues with v7.40 SETOF function
7731 // infrastructure.
7732 // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
7733 // and keep going.
7734 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
7735 {
7736 self.advance();
7737 let e = self.parse_expr(0)?;
7738 return Ok(PlPgSqlStmt::ReturnNext(e));
7739 }
7740 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
7741 {
7742 self.advance();
7743 // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
7744 // rows go to the set, like the static form. It used to desugar to a
7745 // bare ExecuteDynamic, whose result was DISCARDED.
7746 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7747 {
7748 self.advance();
7749 let sql = self.parse_expr(0)?;
7750 return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
7751 }
7752 // Bare RETURN QUERY <select>. If the current token is
7753 // not already SELECT (e.g., the user wrote `RETURN QUERY
7754 // <projection> FROM ...` in a shorthand — rare but PG
7755 // accepts a bare projection here), splice one in. Same
7756 // trick as PERFORM.
7757 if !matches!(self.peek(), Token::Select) {
7758 self.tokens.insert(self.pos, Token::Select);
7759 }
7760 let select = self.parse_select_stmt()?;
7761 let Statement::Select(s) = select else {
7762 return Err(self.err(alloc::format!(
7763 "expected SELECT body after RETURN QUERY, got {:?}",
7764 self.peek()
7765 )));
7766 };
7767 // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
7768 // to an embedded side-effect SELECT whose rows were DISCARDED, which
7769 // in a SETOF function is the entire answer thrown away.
7770 return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
7771 }
7772 // Fall through: parse a full expression.
7773 let e = self.parse_expr(0)?;
7774 Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
7775 }
7776
7777 fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
7778 // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
7779 // are ident-shaped (the parser keys off case-insensitive
7780 // match — same shape used by the top-level Update / Delete
7781 // dispatchers at parse_one_statement).
7782 if matches!(self.peek(), Token::Insert) {
7783 self.advance();
7784 return Ok(TriggerEvent::Insert);
7785 }
7786 match self.peek() {
7787 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7788 self.advance();
7789 Ok(TriggerEvent::Update)
7790 }
7791 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7792 self.advance();
7793 Ok(TriggerEvent::Delete)
7794 }
7795 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
7796 self.advance();
7797 Ok(TriggerEvent::Truncate)
7798 }
7799 other => Err(self.err(alloc::format!(
7800 "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
7801 ))),
7802 }
7803 }
7804
7805 /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
7806 /// - (no clause) → implicit `FOR ALL TABLES`
7807 /// - `FOR ALL TABLES`
7808 /// - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
7809 /// - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
7810 /// accepted as an SPG lenience. PG18-measured (round 753): PG
7811 /// REJECTS the bare plural (`invalid publication object list`,
7812 /// TABLES only pairs with IN SCHEMA); the old note claimed an
7813 /// unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
7814 fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
7815 let name = self.expect_ident_or_string()?;
7816 // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
7817 // shape so existing publications keep parsing identically.
7818 let scope = if matches!(self.peek(), Token::For) {
7819 self.advance();
7820 if matches!(self.peek(), Token::All) {
7821 self.advance();
7822 if !matches!(self.peek(), Token::Tables) {
7823 return Err(self.err(format!(
7824 "expected TABLES after FOR ALL, got {:?}",
7825 self.peek()
7826 )));
7827 }
7828 self.advance();
7829 if matches!(self.peek(), Token::Except) {
7830 self.advance();
7831 let tables = self.parse_publication_table_list()?;
7832 PublicationScope::AllTablesExcept(tables)
7833 } else {
7834 PublicationScope::AllTables
7835 }
7836 } else if matches!(self.peek(), Token::Table) {
7837 self.advance();
7838 let tables = self.parse_publication_table_list()?;
7839 PublicationScope::ForTables(tables)
7840 } else if matches!(self.peek(), Token::Tables) {
7841 // v7.39 (round 754, F31-B5) — PG18-measured: the bare
7842 // plural (`FOR TABLES t`) is REJECTED (`invalid
7843 // publication object list`); TABLES only pairs with
7844 // `IN SCHEMA`. The old arm accepted it on an
7845 // unverifiable "PG 19 accepts both" claim.
7846 self.advance();
7847 if !matches!(self.peek(), Token::In) {
7848 return Err(self.err(alloc::string::String::from(
7849 "invalid publication object list",
7850 )));
7851 }
7852 self.advance();
7853 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
7854 return Err(self.err(format!(
7855 "expected SCHEMA after FOR TABLES IN, got {:?}",
7856 self.peek()
7857 )));
7858 }
7859 self.advance();
7860 let schema = self.expect_ident_or_string()?;
7861 PublicationScope::TablesInSchema(schema)
7862 } else {
7863 return Err(self.err(format!(
7864 "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
7865 self.peek()
7866 )));
7867 }
7868 } else {
7869 PublicationScope::AllTables
7870 };
7871 Ok(Statement::CreatePublication(CreatePublicationStatement {
7872 name,
7873 scope,
7874 }))
7875 }
7876
7877 /// v6.1.3 — Comma-separated identifier list for the publication
7878 /// FOR-clause. Requires at least one entry; empty list is a
7879 /// parse error (PG behaviour). Quoted idents are accepted; the
7880 /// names round-trip through `Display` as `quote_ident(name)`.
7881 ///
7882 /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
7883 /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
7884 /// pg_dump output. SPG's publication state today is per-table
7885 /// only (matching the pre-PG-15 surface); the col list + WHERE
7886 /// are parsed so dumps load through and the table name reaches
7887 /// `PublicationScope::ForTables`, but the filter is not enforced
7888 /// at publish time. Re-open when a customer dogfood gate
7889 /// requires per-row-filter or column-subset publish semantics
7890 /// (which gates on persistent slot state landing first, 21.12).
7891 fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
7892 let first = self.parse_publication_table_entry()?;
7893 let mut out = alloc::vec![first];
7894 while matches!(self.peek(), Token::Comma) {
7895 self.advance();
7896 out.push(self.parse_publication_table_entry()?);
7897 }
7898 Ok(out)
7899 }
7900
7901 /// One table entry inside a FOR TABLE clause:
7902 /// tab_name [ (col, col, …) ] [ WHERE (predicate) ]
7903 /// Returns just the table name; the column list + WHERE predicate
7904 /// are consumed and discarded per the parse-accept-discard
7905 /// commitment above.
7906 fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
7907 let name = self.expect_ident_like()?;
7908 // Optional column list — `(col, col, …)`.
7909 if matches!(self.peek(), Token::LParen) {
7910 self.advance();
7911 // Empty parens are a PG error too; require ≥ 1 column.
7912 let _ = self.expect_ident_like()?;
7913 while matches!(self.peek(), Token::Comma) {
7914 self.advance();
7915 let _ = self.expect_ident_like()?;
7916 }
7917 if !matches!(self.peek(), Token::RParen) {
7918 return Err(self.err(alloc::format!(
7919 "expected ')' to close publication column list, got {:?}",
7920 self.peek()
7921 )));
7922 }
7923 self.advance();
7924 }
7925 // Optional row filter — `WHERE (predicate)`.
7926 if matches!(self.peek(), Token::Where) {
7927 self.advance();
7928 if !matches!(self.peek(), Token::LParen) {
7929 return Err(self.err(alloc::format!(
7930 "expected '(' after WHERE in publication row filter, got {:?}",
7931 self.peek()
7932 )));
7933 }
7934 self.advance();
7935 let _ = self.parse_expr(0)?;
7936 if !matches!(self.peek(), Token::RParen) {
7937 return Err(self.err(alloc::format!(
7938 "expected ')' to close publication WHERE filter, got {:?}",
7939 self.peek()
7940 )));
7941 }
7942 self.advance();
7943 }
7944 Ok(name)
7945 }
7946
7947 /// v6.1.4 — `CREATE SUBSCRIPTION <name>
7948 /// CONNECTION '<conn>'
7949 /// PUBLICATION <pub> [, <pub> ...]`.
7950 ///
7951 /// The clause order is fixed (CONNECTION first, then
7952 /// PUBLICATION) to match PG. No WITH-options accepted in
7953 /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
7954 fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
7955 let name = self.expect_ident_or_string()?;
7956 if !matches!(self.peek(), Token::Connection) {
7957 return Err(self.err(format!(
7958 "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
7959 self.peek()
7960 )));
7961 }
7962 self.advance();
7963 let conn_str = self.expect_string_literal()?;
7964 if !matches!(self.peek(), Token::Publication) {
7965 return Err(self.err(format!(
7966 "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
7967 self.peek()
7968 )));
7969 }
7970 self.advance();
7971 // Reuse the publication FOR-list parser shape: at least one
7972 // identifier, comma-separated.
7973 let first = self.expect_ident_like()?;
7974 let mut publications = alloc::vec![first];
7975 while matches!(self.peek(), Token::Comma) {
7976 self.advance();
7977 publications.push(self.expect_ident_like()?);
7978 }
7979 Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
7980 name,
7981 conn_str,
7982 publications,
7983 }))
7984 }
7985
7986 /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
7987 /// All keywords after `WAIT` are bare idents in v6.1.x; no
7988 /// lexer churn. Both `<pos>` and `<ms>` are positive integers
7989 /// that fit `u64`.
7990 /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
7991 /// qualifier is a *namespace* the app owns (`app.user_id`,
7992 /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
7993 /// to discard. So parse the raw segments here instead of
7994 /// `expect_ident_like`, which strips a leading `schema.` qualifier
7995 /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
7996 /// a single segment and round-trip unchanged.
7997 fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
7998 let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
7999 loop {
8000 let seg = match self.advance() {
8001 Token::Ident(s) | Token::QuotedIdent(s) => s,
8002 other if unreserved_keyword_text(&other).is_some() => {
8003 unreserved_keyword_text(&other).unwrap()
8004 }
8005 other => {
8006 return Err(ParseError {
8007 message: format!("expected parameter name, got {other:?}"),
8008 token_pos: self.consumed_pos(),
8009 });
8010 }
8011 };
8012 parts.push(seg);
8013 if matches!(self.peek(), Token::Dot) {
8014 self.advance();
8015 continue;
8016 }
8017 break;
8018 }
8019 Ok(parts.join(".").to_ascii_lowercase())
8020 }
8021
8022 fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8023 Self::parse_set_value_inner(self)
8024 }
8025
8026 fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8027 match self.advance() {
8028 Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8029 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8030 Ok(crate::ast::SetValue::Default)
8031 }
8032 Token::Ident(s) | Token::QuotedIdent(s) => {
8033 let mut accum = s;
8034 while matches!(self.peek(), Token::Dot) {
8035 self.advance();
8036 let next = self.expect_ident_like()?;
8037 accum.push('.');
8038 accum.push_str(&next);
8039 }
8040 Ok(crate::ast::SetValue::Ident(accum))
8041 }
8042 Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8043 Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8044 // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8045 // spellings that lex as keyword tokens, not idents:
8046 // `SET standard_conforming_strings = on` is in every
8047 // pg_dump preamble (`off` already lexes as an ident).
8048 // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8049 // DEFAULT lexes as its keyword token, so the ident arm above
8050 // never saw it and the everyday reset form was a syntax error.
8051 Token::Default => Ok(crate::ast::SetValue::Default),
8052 Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8053 Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8054 Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8055 // v7.14.0 — MySQL session/user variable RHS
8056 // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8057 // Wrap as Ident so the SET handler can record it; the
8058 // engine treats `@VAR` / `@@VAR` values as opaque
8059 // strings.
8060 Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8061 // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8062 // is the common MySQL preamble shape. Allow a `+` or
8063 // `-` prefix on negative numerics for parity with PG
8064 // (some param defaults are negative).
8065 Token::Minus => match self.advance() {
8066 Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8067 Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8068 other => Err(self.err(format!(
8069 "expected numeric after `-` in SET value, got {other:?}"
8070 ))),
8071 },
8072 other => Err(self.err(format!(
8073 "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8074 ))),
8075 }
8076 }
8077
8078 /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8079 /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8080 /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8081 /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8082 /// present). Modes are comma-separated per PG; SPG also
8083 /// accepts space-separated for tolerance. READ ONLY / WRITE
8084 /// / DEFERRABLE are parsed-and-ignored (recorded for future
8085 /// surface but not behaviorally honoured today).
8086 /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8087 /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8088 /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8089 /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8090 /// session default rather than forcing READ COMMITTED.
8091 fn parse_isolation_level_clauses(&mut self) -> Result<Option<IsolationLevel>, ParseError> {
8092 let mut level = IsolationLevel::default();
8093 let mut have_level = false;
8094 loop {
8095 // ISOLATION LEVEL …
8096 let saw_isolation =
8097 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8098 if saw_isolation {
8099 self.advance(); // ISOLATION
8100 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8101 return Err(self.err(alloc::format!(
8102 "expected LEVEL after ISOLATION, got {:?}",
8103 self.peek()
8104 )));
8105 }
8106 self.advance(); // LEVEL
8107 // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8108 let w1 = self
8109 .expect_ident_like()
8110 .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8111 let lc = w1.to_ascii_lowercase();
8112 level = match lc.as_str() {
8113 "serializable" => IsolationLevel::Serializable,
8114 "repeatable" => {
8115 // Expect READ
8116 let w2 = self
8117 .expect_ident_like()
8118 .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8119 if !w2.eq_ignore_ascii_case("read") {
8120 return Err(self.err(alloc::format!(
8121 "expected READ after REPEATABLE, got {w2:?}"
8122 )));
8123 }
8124 IsolationLevel::RepeatableRead
8125 }
8126 "read" => {
8127 let w2 = self
8128 .expect_ident_like()
8129 .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8130 match w2.to_ascii_lowercase().as_str() {
8131 "committed" => IsolationLevel::ReadCommitted,
8132 "uncommitted" => IsolationLevel::ReadUncommitted,
8133 other => {
8134 return Err(self.err(alloc::format!(
8135 "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8136 )));
8137 }
8138 }
8139 }
8140 other => {
8141 return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8142 }
8143 };
8144 have_level = true;
8145 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8146 // READ ONLY | READ WRITE — parsed, not behaviorally honoured.
8147 self.advance();
8148 match self.peek().clone() {
8149 Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8150 self.advance();
8151 }
8152 Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8153 self.advance();
8154 }
8155 other => {
8156 return Err(self.err(alloc::format!(
8157 "expected ONLY or WRITE after READ, got {other:?}"
8158 )));
8159 }
8160 }
8161 } else if matches!(self.peek(), Token::Not) {
8162 // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8163 self.advance();
8164 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8165 return Err(self.err(alloc::format!(
8166 "expected DEFERRABLE after NOT, got {:?}",
8167 self.peek()
8168 )));
8169 }
8170 self.advance();
8171 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8172 {
8173 self.advance();
8174 } else {
8175 break;
8176 }
8177 // Optional comma between modes.
8178 if matches!(self.peek(), Token::Comma) {
8179 self.advance();
8180 }
8181 }
8182 Ok(have_level.then_some(level))
8183 }
8184
8185 fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8186 // FOR is a v6.1.2-reserved keyword (Token::For). The
8187 // other two are bare idents — they've never needed lexer
8188 // support and we keep it that way.
8189 if !matches!(self.peek(), Token::For) {
8190 return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8191 }
8192 self.advance();
8193 self.expect_keyword_ident("wal")?;
8194 self.expect_keyword_ident("position")?;
8195 let pos = self.expect_u64_literal()?;
8196 let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8197 {
8198 self.advance();
8199 self.expect_keyword_ident("timeout")?;
8200 Some(self.expect_u64_literal()?)
8201 } else {
8202 None
8203 };
8204 Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8205 }
8206
8207 /// v6.1.7 helper — consume a `Token::Integer` and check it
8208 /// fits `u64`. WAL positions and millisecond timeouts are
8209 /// non-negative.
8210 fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8211 match self.advance() {
8212 Token::Integer(n) if n >= 0 => Ok(n as u64),
8213 Token::Integer(n) => Err(ParseError {
8214 message: format!("expected non-negative integer, got {n}"),
8215 token_pos: self.consumed_pos(),
8216 }),
8217 other => Err(ParseError {
8218 message: format!("expected integer literal, got {other:?}"),
8219 token_pos: self.consumed_pos(),
8220 }),
8221 }
8222 }
8223
8224 /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8225 /// ROLE '<role>' (defaults to readonly). All string slots accept
8226 /// either a quoted ident or a quoted string literal.
8227 /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8228 /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8229 ///
8230 /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8231 /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8232 /// wire role) still parses — it is a different axis from the PG attributes.
8233 /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8234 /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8235 /// or RESET, so the plain attribute forms keep their old path.
8236 fn peeks_db_role_setting(&self) -> bool {
8237 let mut i = self.pos + 1; // past the object's name
8238 let word = |p: usize| -> Option<String> {
8239 match self.tokens.get(p) {
8240 Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8241 Some(Token::In) => Some(String::from("in")),
8242 _ => None,
8243 }
8244 };
8245 if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8246 i += 3; // IN DATABASE <name>
8247 }
8248 matches!(word(i).as_deref(), Some("set" | "reset"))
8249 }
8250
8251 fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8252 use crate::ast::SetDbRoleSettingStatement;
8253 // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8254 // identifier, so the ordinary name reader refuses it. Same trap
8255 // as TABLE / INDEX / FULL / DEFAULT before it.
8256 let name = if matches!(self.peek(), Token::All) {
8257 self.advance();
8258 String::from("all")
8259 } else {
8260 self.expect_ident_or_string()?
8261 };
8262 // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8263 let all = name.eq_ignore_ascii_case("all");
8264 let (mut database, mut role) = if is_database {
8265 (Some(name), None)
8266 } else if all {
8267 (None, None)
8268 } else {
8269 (None, Some(name))
8270 };
8271 if matches!(self.peek(), Token::In) {
8272 self.advance();
8273 self.advance(); // DATABASE
8274 database = Some(self.expect_ident_or_string()?);
8275 }
8276 let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8277 self.advance(); // SET | RESET
8278 if resetting && matches!(self.peek(), Token::All) {
8279 self.advance();
8280 self.consume_until_statement_boundary();
8281 return Ok(Statement::SetDbRoleSetting(Box::new(
8282 SetDbRoleSettingStatement {
8283 database,
8284 role,
8285 param: None,
8286 value: None,
8287 },
8288 )));
8289 }
8290 let param = self.expect_ident_like()?;
8291 let value = if resetting {
8292 None
8293 } else {
8294 // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8295 // KEYWORD, so the ident-only check missed it and consumed
8296 // the word itself as the value — the same trap as ALL, one
8297 // clause over.
8298 if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8299 self.advance();
8300 }
8301 Some(self.take_guc_value())
8302 };
8303 self.consume_until_statement_boundary();
8304 Ok(Statement::SetDbRoleSetting(Box::new(
8305 SetDbRoleSettingStatement {
8306 database,
8307 role,
8308 param: Some(param),
8309 value,
8310 },
8311 )))
8312 }
8313
8314 /// The remainder of a `SET <p> = …` clause as PG renders it back:
8315 /// a quoted literal loses its quotes, a bare word or number does not.
8316 fn take_guc_value(&mut self) -> String {
8317 match self.advance() {
8318 Token::String(s) => s,
8319 Token::Integer(n) => format!("{n}"),
8320 Token::Float(f) => format!("{f}"),
8321 Token::Ident(s) | Token::QuotedIdent(s) => s,
8322 other => format!("{other:?}"),
8323 }
8324 }
8325
8326 fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8327 let name = self.expect_ident_or_string()?;
8328 if self.peek_keyword_ident("with") {
8329 self.advance();
8330 }
8331 let mut password = String::new();
8332 let mut role = String::new();
8333 let mut login: Option<bool> = None;
8334 let mut inherit: Option<bool> = None;
8335 let mut superuser: Option<bool> = None;
8336 // Not a `while let`: the pattern would borrow `self` across the
8337 // body, which calls `self.advance()` / `self.expect_*` (&mut).
8338 #[allow(clippy::while_let_loop)]
8339 loop {
8340 let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8341 break;
8342 };
8343 match w.to_ascii_lowercase().as_str() {
8344 "password" => {
8345 self.advance();
8346 password = self.expect_string_literal()?;
8347 }
8348 // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8349 // is the same slot.
8350 "encrypted" => {
8351 self.advance();
8352 self.expect_keyword_ident("password")?;
8353 password = self.expect_string_literal()?;
8354 }
8355 "login" => {
8356 self.advance();
8357 login = Some(true);
8358 }
8359 "nologin" => {
8360 self.advance();
8361 login = Some(false);
8362 }
8363 "inherit" => {
8364 self.advance();
8365 inherit = Some(true);
8366 }
8367 "noinherit" => {
8368 self.advance();
8369 inherit = Some(false);
8370 }
8371 "superuser" => {
8372 self.advance();
8373 superuser = Some(true);
8374 }
8375 "nosuperuser" => {
8376 self.advance();
8377 superuser = Some(false);
8378 }
8379 // SPG's own coarse wire role: `ROLE 'readwrite'`.
8380 "role" => {
8381 self.advance();
8382 role = self.expect_string_literal()?;
8383 }
8384 // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8385 // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8386 // accepted and ignored so a pg_dump role block restores. They
8387 // gate capabilities SPG does not have.
8388 "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8389 | "noreplication" | "bypassrls" | "nobypassrls" => {
8390 self.advance();
8391 }
8392 "connection" => {
8393 self.advance();
8394 self.expect_keyword_ident("limit")?;
8395 self.advance(); // the number
8396 }
8397 "valid" => {
8398 self.advance();
8399 self.expect_keyword_ident("until")?;
8400 self.expect_string_literal()?;
8401 }
8402 _ => break,
8403 }
8404 }
8405 if role.is_empty() {
8406 role = "readonly".to_string();
8407 }
8408 Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8409 name,
8410 password,
8411 role,
8412 login,
8413 inherit,
8414 superuser,
8415 is_user,
8416 }))
8417 }
8418
8419 /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8420 /// consumed the USING / WITH CHECK keyword.
8421 fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8422 if !matches!(self.peek(), Token::LParen) {
8423 return Err(self.err(alloc::format!(
8424 "expected '(' after {clause}, got {:?}",
8425 self.peek()
8426 )));
8427 }
8428 self.advance();
8429 let e = self.parse_expr(0)?;
8430 if !matches!(self.peek(), Token::RParen) {
8431 return Err(self.err(alloc::format!(
8432 "expected ')' to close {clause}, got {:?}",
8433 self.peek()
8434 )));
8435 }
8436 self.advance();
8437 Ok(e)
8438 }
8439
8440 /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8441 fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8442 let mut roles = Vec::new();
8443 loop {
8444 roles.push(self.expect_ident_like()?);
8445 if matches!(self.peek(), Token::Comma) {
8446 self.advance();
8447 } else {
8448 break;
8449 }
8450 }
8451 Ok(roles)
8452 }
8453
8454 /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8455 /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8456 /// `CREATE POLICY`.
8457 fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8458 use crate::ast::PolicyCmd;
8459 let name = self.expect_ident_like()?;
8460 if !matches!(self.peek(), Token::On) {
8461 return Err(self.err(alloc::format!(
8462 "expected ON after CREATE POLICY name, got {:?}",
8463 self.peek()
8464 )));
8465 }
8466 self.advance();
8467 let table = self.expect_ident_like()?;
8468
8469 let mut permissive = true;
8470 if matches!(self.peek(), Token::As) {
8471 self.advance();
8472 let w = self.expect_ident_like()?;
8473 permissive = if w.eq_ignore_ascii_case("permissive") {
8474 true
8475 } else if w.eq_ignore_ascii_case("restrictive") {
8476 false
8477 } else {
8478 return Err(self.err(alloc::format!(
8479 "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8480 )));
8481 };
8482 }
8483
8484 let mut cmd = PolicyCmd::All;
8485 if matches!(self.peek(), Token::For) {
8486 self.advance();
8487 cmd = self.parse_policy_cmd()?;
8488 }
8489
8490 let mut roles = Vec::new();
8491 if matches!(self.peek(), Token::To) {
8492 self.advance();
8493 roles = self.parse_policy_roles()?;
8494 }
8495
8496 let mut using = None;
8497 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8498 {
8499 self.advance();
8500 using = Some(self.parse_paren_expr("USING")?);
8501 }
8502
8503 let mut with_check = None;
8504 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8505 {
8506 self.advance();
8507 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8508 {
8509 return Err(self.err(alloc::format!(
8510 "expected CHECK after WITH, got {:?}",
8511 self.peek()
8512 )));
8513 }
8514 self.advance();
8515 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8516 }
8517
8518 // Clause-per-command matrix (PG wording).
8519 match cmd {
8520 PolicyCmd::Insert => {
8521 if using.is_some() {
8522 return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8523 }
8524 }
8525 PolicyCmd::Select | PolicyCmd::Delete => {
8526 if with_check.is_some() {
8527 return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8528 }
8529 }
8530 PolicyCmd::Update | PolicyCmd::All => {}
8531 }
8532
8533 Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8534 name,
8535 table,
8536 permissive,
8537 cmd,
8538 roles,
8539 using,
8540 with_check,
8541 }))
8542 }
8543
8544 /// v7.39 (RLS) — the command word after `FOR`.
8545 fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8546 use crate::ast::PolicyCmd;
8547 match self.peek().clone() {
8548 Token::All => {
8549 self.advance();
8550 Ok(PolicyCmd::All)
8551 }
8552 Token::Select => {
8553 self.advance();
8554 Ok(PolicyCmd::Select)
8555 }
8556 Token::Insert => {
8557 self.advance();
8558 Ok(PolicyCmd::Insert)
8559 }
8560 Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8561 self.advance();
8562 Ok(PolicyCmd::Update)
8563 }
8564 Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8565 self.advance();
8566 Ok(PolicyCmd::Delete)
8567 }
8568 other => Err(self.err(alloc::format!(
8569 "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8570 ))),
8571 }
8572 }
8573
8574 /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8575 /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8576 fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8577 let name = self.expect_ident_like()?;
8578 if !matches!(self.peek(), Token::On) {
8579 return Err(self.err(alloc::format!(
8580 "expected ON after ALTER POLICY name, got {:?}",
8581 self.peek()
8582 )));
8583 }
8584 self.advance();
8585 let table = self.expect_ident_like()?;
8586
8587 // RENAME TO new
8588 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8589 {
8590 self.advance();
8591 if !matches!(self.peek(), Token::To) {
8592 return Err(self.err(alloc::format!(
8593 "expected TO after RENAME, got {:?}",
8594 self.peek()
8595 )));
8596 }
8597 self.advance();
8598 let new = self.expect_ident_like()?;
8599 return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8600 name,
8601 table,
8602 rename_to: Some(new),
8603 roles: None,
8604 using: None,
8605 with_check: None,
8606 }));
8607 }
8608
8609 let mut roles = None;
8610 if matches!(self.peek(), Token::To) {
8611 self.advance();
8612 roles = Some(self.parse_policy_roles()?);
8613 }
8614 let mut using = None;
8615 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8616 {
8617 self.advance();
8618 using = Some(self.parse_paren_expr("USING")?);
8619 }
8620 let mut with_check = None;
8621 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8622 {
8623 self.advance();
8624 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8625 {
8626 return Err(self.err(alloc::format!(
8627 "expected CHECK after WITH, got {:?}",
8628 self.peek()
8629 )));
8630 }
8631 self.advance();
8632 with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8633 }
8634 Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8635 name,
8636 table,
8637 rename_to: None,
8638 roles,
8639 using,
8640 with_check,
8641 }))
8642 }
8643
8644 /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
8645 /// `DROP POLICY`.
8646 fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8647 let if_exists = self.consume_if_exists();
8648 let name = self.expect_ident_like()?;
8649 if !matches!(self.peek(), Token::On) {
8650 return Err(self.err(alloc::format!(
8651 "expected ON after DROP POLICY name, got {:?}",
8652 self.peek()
8653 )));
8654 }
8655 self.advance();
8656 let table = self.expect_ident_like()?;
8657 Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
8658 name,
8659 table,
8660 if_exists,
8661 }))
8662 }
8663}
8664fn wrap_from_leaves(
8665 e: &mut Expr,
8666 names: &[String],
8667 make: &dyn Fn(Expr) -> Expr,
8668 refs: &dyn Fn(&Expr) -> bool,
8669) {
8670 if let Expr::Column(c) = e {
8671 if c.qualifier
8672 .as_deref()
8673 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
8674 {
8675 let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
8676 *e = make(taken);
8677 }
8678 return;
8679 }
8680 match e {
8681 Expr::Binary { lhs, rhs, .. } => {
8682 wrap_from_leaves(lhs, names, make, refs);
8683 wrap_from_leaves(rhs, names, make, refs);
8684 }
8685 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
8686 wrap_from_leaves(expr, names, make, refs)
8687 }
8688 Expr::FunctionCall { args, .. } => {
8689 for a in args.iter_mut() {
8690 wrap_from_leaves(a, names, make, refs);
8691 }
8692 }
8693 Expr::Case {
8694 operand,
8695 branches,
8696 else_branch,
8697 } => {
8698 if let Some(o) = operand.as_deref_mut() {
8699 wrap_from_leaves(o, names, make, refs);
8700 }
8701 for (w, t) in branches.iter_mut() {
8702 wrap_from_leaves(w, names, make, refs);
8703 wrap_from_leaves(t, names, make, refs);
8704 }
8705 if let Some(el) = else_branch.as_deref_mut() {
8706 wrap_from_leaves(el, names, make, refs);
8707 }
8708 }
8709 // Compound variants the walk doesn't decompose: keep the
8710 // pre-D.30 behavior — wrap the whole sub-expr if it touches
8711 // a source table, so nothing regresses.
8712 other => {
8713 if refs(other) {
8714 let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
8715 *other = make(taken);
8716 }
8717 }
8718 }
8719}
8720
8721/// v7.39 (round 241) — does this expression reference any of the FROM /
8722/// USING table names (shared by the UPDATE…FROM and DELETE…USING
8723/// lowerings)?
8724fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
8725 match e {
8726 Expr::Column(c) => c
8727 .qualifier
8728 .as_deref()
8729 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
8730 Expr::Binary { lhs, rhs, .. } => {
8731 expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
8732 }
8733 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
8734 Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
8735 Expr::Case {
8736 operand,
8737 branches,
8738 else_branch,
8739 } => {
8740 operand
8741 .as_deref()
8742 .is_some_and(|o| expr_refs_tables(o, names))
8743 || branches
8744 .iter()
8745 .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
8746 || else_branch
8747 .as_deref()
8748 .is_some_and(|el| expr_refs_tables(el, names))
8749 }
8750 _ => false,
8751 }
8752}
8753
8754impl Parser {
8755 /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
8756 /// Caller already consumed the leading `UPDATE` ident.
8757 /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
8758 /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
8759 /// after the target name has been read. `JOIN` is a reserved token;
8760 /// the qualifiers are bare idents.
8761 fn peek_is_update_join_start(&self) -> bool {
8762 match self.peek() {
8763 // JOIN and its qualifiers are reserved lexer tokens (the grammar
8764 // dedicates arms to `LEFT [OUTER] JOIN` and friends).
8765 Token::Join
8766 | Token::Inner
8767 | Token::Left
8768 | Token::Right
8769 | Token::Cross
8770 | Token::Full => true,
8771 // NATURAL / STRAIGHT_JOIN arrive as bare idents.
8772 Token::Ident(s) | Token::QuotedIdent(s) => {
8773 matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
8774 }
8775 _ => false,
8776 }
8777 }
8778
8779 /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
8780 /// USER-variable assignment. Its own per-session namespace, an arbitrary
8781 /// expression on the right, and `:=` as a second spelling of `=`.
8782 ///
8783 /// Out-of-line (`inline(never)`): the statement-parse frame it is called
8784 /// from sits on the nesting recursion chain (a CTE body, a subquery),
8785 /// and holding this loop's `Vec` + `String` locals there overflowed the
8786 /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
8787 #[inline(never)]
8788 fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
8789 let mut assigns: Vec<(String, Expr)> = Vec::new();
8790 let mut settings: Vec<(String, Expr)> = Vec::new();
8791 loop {
8792 // v7.39 (round 554) — a plain NAME here is a session
8793 // setting, not a user variable. mysqldump writes the two in
8794 // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
8795 // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
8796 // changes it — and this refused the mixture outright, so no
8797 // dump could be restored past its preamble.
8798 if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
8799 self.advance();
8800 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8801 return Err(self.err(alloc::format!(
8802 "expected `=` after {name}, got {:?}",
8803 self.peek()
8804 )));
8805 }
8806 self.advance();
8807 let value = self.parse_expr(0)?;
8808 settings.push((name.to_ascii_lowercase(), value));
8809 if matches!(self.peek(), Token::Comma) {
8810 self.advance();
8811 continue;
8812 }
8813 break;
8814 }
8815 let Token::SessionVar(raw) = self.peek().clone() else {
8816 return Err(self.err(alloc::format!(
8817 "expected a user variable after SET, got {:?}",
8818 self.peek()
8819 )));
8820 };
8821 if raw.starts_with("@@") {
8822 return Err(self.err(alloc::string::String::from(
8823 "cannot mix `@@` settings with `@` user variables in one SET",
8824 )));
8825 }
8826 self.advance();
8827 if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
8828 return Err(self.err(alloc::format!(
8829 "expected `=` or `:=` after {raw}, got {:?}",
8830 self.peek()
8831 )));
8832 }
8833 self.advance();
8834 let value = self.parse_expr(0)?;
8835 assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
8836 if matches!(self.peek(), Token::Comma) {
8837 self.advance();
8838 continue;
8839 }
8840 break;
8841 }
8842 Ok(Statement::SetUserVars(assigns, settings))
8843 }
8844
8845 fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
8846 // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
8847 // NAMED `only` until now, which failed on `relation "only" does
8848 // not exist`. The lookahead is what keeps a table actually
8849 // called `only` working: the keyword is only a keyword when a
8850 // TABLE NAME follows it — and `SET` arrives as an identifier
8851 // here, so `UPDATE only SET a = 2` would otherwise take `SET`
8852 // for the table and die on the `=`. Measured by the pin.
8853 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
8854 if s.eq_ignore_ascii_case("only"))
8855 && matches!(
8856 self.tokens.get(self.pos + 1),
8857 Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
8858 );
8859 if only {
8860 self.advance();
8861 }
8862 let table = self.expect_ident_like()?;
8863 // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
8864 // bare spelling; a bare identifier that is the SET keyword itself
8865 // is the clause, not an alias.
8866 // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
8867 // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
8868 // multi-table form, and swallowing `LEFT` as `a`'s alias made the
8869 // following JOIN a syntax error.
8870 let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
8871 let alias = if matches!(self.peek(), Token::As) {
8872 self.advance();
8873 Some(self.expect_ident_like()?)
8874 } else {
8875 match self.peek() {
8876 Token::Ident(s) | Token::QuotedIdent(s)
8877 if !s.eq_ignore_ascii_case("set") && !starts_join =>
8878 {
8879 let a = s.clone();
8880 self.advance();
8881 Some(a)
8882 }
8883 _ => None,
8884 }
8885 };
8886 // v7.39 (round 420) — MySQL's multi-table UPDATE:
8887 // UPDATE a, b SET a.v = b.v WHERE a.id = b.id
8888 // UPDATE a JOIN b ON a.id = b.id SET a.v = b.v + 1
8889 // UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
8890 // The FIRST table is the mutation target and the rest are sources —
8891 // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
8892 // SPG already lowers onto correlated subqueries. So rewind, let
8893 // `parse_from_clause` read the whole list (it handles aliases, comma
8894 // lists, and every JOIN form), then peel the target off the front.
8895 let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
8896 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
8897 {
8898 // NOTE: `advance()` destroys the tokens it returns
8899 // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
8900 // is NOT possible — the tail is read forward, once, through the
8901 // same grammar `parse_from_clause` uses after its primary.
8902 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
8903 let mut joins = self.parse_from_joins(&target_qual)?;
8904 if joins.is_empty() {
8905 return Err(self.err(alloc::string::String::from(
8906 "multi-table UPDATE needs at least one source table",
8907 )));
8908 }
8909 let head = joins.remove(0);
8910 // A LEFT join keeps every target row (the unmatched ones see NULL
8911 // on the source side), so it must NOT get the EXISTS row filter
8912 // the inner / comma forms use.
8913 let outer = matches!(head.kind, crate::ast::JoinKind::Left);
8914 let src = FromClause {
8915 primary: head.table,
8916 joins,
8917 };
8918 (Some(src), head.on, outer)
8919 } else {
8920 (None, None, false)
8921 };
8922 self.expect_keyword_ident("set")?;
8923 let mut assignments = Vec::new();
8924 loop {
8925 // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
8926 // …)` — the parenthesized multi-assignment. Expressions
8927 // assign positionally; a subquery RHS clones per column
8928 // keeping only the Nth projection item.
8929 if matches!(self.peek(), Token::LParen) {
8930 self.advance();
8931 let mut cols = alloc::vec![self.expect_ident_like()?];
8932 while matches!(self.peek(), Token::Comma) {
8933 self.advance();
8934 cols.push(self.expect_ident_like()?);
8935 }
8936 if !matches!(self.peek(), Token::RParen) {
8937 return Err(self.err(format!(
8938 "expected ')' after SET column list, got {:?}",
8939 self.peek()
8940 )));
8941 }
8942 self.advance();
8943 if !matches!(self.peek(), Token::Eq) {
8944 return Err(self.err(format!(
8945 "expected `=` after SET column list, got {:?}",
8946 self.peek()
8947 )));
8948 }
8949 self.advance();
8950 if !matches!(self.peek(), Token::LParen) {
8951 return Err(self.err(format!(
8952 "expected '(' after SET (…) =, got {:?}",
8953 self.peek()
8954 )));
8955 }
8956 self.advance();
8957 if matches!(self.peek(), Token::Select) {
8958 let inner = match self.parse_select_stmt()? {
8959 Statement::Select(s) => s,
8960 other => {
8961 return Err(self.err(alloc::format!(
8962 "expected SELECT in SET (…) = (SELECT …), got {other:?}"
8963 )));
8964 }
8965 };
8966 if !matches!(self.peek(), Token::RParen) {
8967 return Err(self.err(format!(
8968 "expected ')' after SET subquery, got {:?}",
8969 self.peek()
8970 )));
8971 }
8972 self.advance();
8973 if inner.items.len() != cols.len() {
8974 return Err(self.err(alloc::format!(
8975 "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
8976 cols.len(),
8977 inner.items.len()
8978 )));
8979 }
8980 for (i, col) in cols.into_iter().enumerate() {
8981 let mut sub = inner.clone();
8982 sub.items = alloc::vec![sub.items[i].clone()];
8983 assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
8984 }
8985 } else {
8986 let mut exprs = alloc::vec![self.parse_expr(0)?];
8987 while matches!(self.peek(), Token::Comma) {
8988 self.advance();
8989 exprs.push(self.parse_expr(0)?);
8990 }
8991 if !matches!(self.peek(), Token::RParen) {
8992 return Err(self.err(format!(
8993 "expected ')' after SET row values, got {:?}",
8994 self.peek()
8995 )));
8996 }
8997 self.advance();
8998 if exprs.len() != cols.len() {
8999 return Err(self.err(alloc::format!(
9000 "SET (…) = (…) arity mismatch: {} columns, {} values",
9001 cols.len(),
9002 exprs.len()
9003 )));
9004 }
9005 for (col, e) in cols.into_iter().zip(exprs) {
9006 assignments.push((col, e));
9007 }
9008 }
9009 if matches!(self.peek(), Token::Comma) {
9010 self.advance();
9011 continue;
9012 }
9013 break;
9014 }
9015 // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9016 // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9017 // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9018 // `public.` dump qualifiers), so the qualifier has to be read off
9019 // the token stream first — otherwise `SET b.v = 888` would write
9020 // to the TARGET table's `v` while naming a source table, a
9021 // silent-wrong. A qualifier naming a SOURCE table means a
9022 // multi-TARGET update — mutating two tables in one statement —
9023 // which SPG does not model, so it is refused loudly.
9024 let set_qual: Option<String> = if mysql_from.is_some()
9025 && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9026 {
9027 match self.peek() {
9028 Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9029 _ => None,
9030 }
9031 } else {
9032 None
9033 };
9034 let col = self.expect_ident_like()?;
9035 if let Some(q) = set_qual {
9036 let names_target = q.eq_ignore_ascii_case(&table)
9037 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9038 if !names_target {
9039 return Err(self.err(alloc::format!(
9040 "multi-table UPDATE can only assign to its first table \
9041 ({table}); `{q}.{col}` targets another table"
9042 )));
9043 }
9044 }
9045 // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9046 // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9047 // `__column_default` marker lowering just below). PG assigns to the
9048 // i-th (1-based) element, NULL-padding when i exceeds the length.
9049 if matches!(self.peek(), Token::LBracket) {
9050 self.advance();
9051 let index = self.parse_expr(0)?;
9052 // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9053 // (and the open `arr[lo:]`), lowered to
9054 // `__array_assign_slice`. Only the single-subscript form
9055 // parsed before, so a slice assignment was a syntax error.
9056 let mut slice_hi: Option<Option<Expr>> = None;
9057 if matches!(self.peek(), Token::Colon) {
9058 self.advance();
9059 slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9060 None
9061 } else {
9062 Some(self.parse_expr(0)?)
9063 });
9064 }
9065 if !matches!(self.peek(), Token::RBracket) {
9066 return Err(self.err(format!(
9067 "expected `]` after array subscript in UPDATE SET, got {:?}",
9068 self.peek()
9069 )));
9070 }
9071 self.advance();
9072 if !matches!(self.peek(), Token::Eq) {
9073 return Err(self.err(format!(
9074 "expected `=` after array subscript in UPDATE SET, got {:?}",
9075 self.peek()
9076 )));
9077 }
9078 self.advance();
9079 let value = self.parse_expr(0)?;
9080 // PG merges several subscript writes to the same column into one
9081 // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9082 // assignment to `col` rather than each overwriting the original.
9083 let existing = assignments.iter().position(|(c, _)| c == &col);
9084 let base = match existing {
9085 Some(i) => assignments[i].1.clone(),
9086 None => Expr::Column(ColumnName {
9087 qualifier: None,
9088 name: col.clone(),
9089 }),
9090 };
9091 let assigned = match slice_hi {
9092 None => Expr::FunctionCall {
9093 name: "__array_assign".to_string(),
9094 args: alloc::vec![base, index, value],
9095 },
9096 Some(hi) => Expr::FunctionCall {
9097 name: "__array_assign_slice".to_string(),
9098 args: alloc::vec![
9099 base,
9100 index,
9101 hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9102 value,
9103 ],
9104 },
9105 };
9106 match existing {
9107 Some(i) => assignments[i].1 = assigned,
9108 None => assignments.push((col, assigned)),
9109 }
9110 if matches!(self.peek(), Token::Comma) {
9111 self.advance();
9112 continue;
9113 }
9114 break;
9115 }
9116 if !matches!(self.peek(), Token::Eq) {
9117 return Err(self.err(format!(
9118 "expected `=` after column name in UPDATE SET, got {:?}",
9119 self.peek()
9120 )));
9121 }
9122 self.advance();
9123 // `SET col = DEFAULT` — the column's declared default;
9124 // rides out as a marker call the update executor
9125 // resolves against the schema.
9126 let value = if matches!(self.peek(), Token::Default) {
9127 self.advance();
9128 Expr::FunctionCall {
9129 name: "__column_default".to_string(),
9130 args: Vec::new(),
9131 }
9132 } else {
9133 self.parse_expr(0)?
9134 };
9135 assignments.push((col, value));
9136 if matches!(self.peek(), Token::Comma) {
9137 self.advance();
9138 continue;
9139 }
9140 break;
9141 }
9142 // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9143 // update. Lowers onto the correlated-subquery machinery:
9144 // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9145 // and each assignment that references a FROM-list table
9146 // wraps into a correlated scalar subquery
9147 // (SELECT expr FROM src WHERE cond). Equivalent for the
9148 // unique-join shape (the overwhelmingly common one); a
9149 // multi-match, which PG resolves by arbitrary pick,
9150 // surfaces as a scalar-subquery cardinality error instead
9151 // of a silent arbitrary result.
9152 // v7.39 (round 420) — the MySQL multi-table form supplies the source
9153 // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9154 // the SAME lowering below. Both spellings together is not legal in
9155 // either dialect.
9156 let from_clause = if let Some(fc) = mysql_from {
9157 if matches!(self.peek(), Token::From) {
9158 return Err(self.err(alloc::string::String::from(
9159 "multi-table UPDATE already names its sources; drop the FROM clause",
9160 )));
9161 }
9162 Some(fc)
9163 } else if matches!(self.peek(), Token::From) {
9164 self.advance();
9165 Some(self.parse_from_clause()?)
9166 } else {
9167 None
9168 };
9169 let where_ = if matches!(self.peek(), Token::Where) {
9170 self.advance();
9171 Some(self.parse_expr(0)?)
9172 } else {
9173 None
9174 };
9175 // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9176 // and the TARGET-row filter are NOT the same predicate once a LEFT
9177 // join is involved:
9178 // * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9179 // one conjunction, and the whole thing filters target rows via
9180 // EXISTS.
9181 // * LEFT join: only the ON predicate belongs inside the source
9182 // subquery. The WHERE still filters TARGET rows (with source
9183 // columns read through the correlated subquery, which yields NULL
9184 // for an unmatched row — exactly LEFT-join semantics).
9185 // Round 420 folded ON into WHERE unconditionally and then dropped the
9186 // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9187 // WHERE a.id > 1` updated EVERY row.
9188 let sub_where = match (mysql_on.clone(), where_.clone()) {
9189 _ if mysql_outer => mysql_on.clone(),
9190 (Some(on), Some(w)) => Some(Expr::Binary {
9191 lhs: Box::new(on),
9192 op: crate::ast::BinOp::And,
9193 rhs: Box::new(w),
9194 }),
9195 (Some(on), None) => Some(on),
9196 (None, w) => w,
9197 };
9198 // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9199 // has no such clause on UPDATE, so this is accepted only under the
9200 // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9201 let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9202 let mut returning = self.parse_optional_returning()?;
9203 // v7.39 (round 533) — kept for the engine, which can resolve the
9204 // UNQUALIFIED leaves this lowering has to leave alone.
9205 let from_sources = from_clause.as_ref().map(|fc| {
9206 alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9207 from: fc.clone(),
9208 sub_where: sub_where.clone(),
9209 })
9210 });
9211 let (assignments, where_) = if let Some(fc) = from_clause {
9212 let names: Vec<String> = core::iter::once(&fc.primary)
9213 .chain(fc.joins.iter().map(|j| &j.table))
9214 .flat_map(|t| {
9215 t.alias
9216 .clone()
9217 .into_iter()
9218 .chain(core::iter::once(t.name.clone()))
9219 })
9220 .collect();
9221 let refs_list = |e: &Expr| -> bool {
9222 fn walk(e: &Expr, names: &[String]) -> bool {
9223 match e {
9224 Expr::Column(c) => c
9225 .qualifier
9226 .as_deref()
9227 .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9228 Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9229 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9230 Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9231 Expr::Case {
9232 operand,
9233 branches,
9234 else_branch,
9235 } => {
9236 operand.as_deref().is_some_and(|o| walk(o, names))
9237 || branches
9238 .iter()
9239 .any(|(w, t)| walk(w, names) || walk(t, names))
9240 || else_branch.as_deref().is_some_and(|el| walk(el, names))
9241 }
9242 _ => false,
9243 }
9244 }
9245 walk(e, &names)
9246 };
9247 let sub_select = |items: Vec<SelectItem>| SelectStatement {
9248 locking: None,
9249 ctes: Vec::new(),
9250 distinct: false,
9251 distinct_on: Vec::new(),
9252 items,
9253 from: Some(fc.clone()),
9254 where_: sub_where.clone(),
9255 group_by: None,
9256 group_by_all: false,
9257 having: None,
9258 unions: Vec::new(),
9259 order_by: Vec::new(),
9260 limit: None,
9261 offset: None,
9262 limit_with_ties: false,
9263 window_check_exprs: Vec::new(),
9264 };
9265 // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9266 // assignment RHS with a correlated scalar subquery, instead of
9267 // wrapping the whole RHS. Wrapping the whole expr moved a target-
9268 // column reference (`SET v = v + u.bonus`, where `v` is the target
9269 // table's column) inside a subquery whose FROM only has the source
9270 // table, so the unqualified `v` resolved against the source and
9271 // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9272 // context — where they belong — fixes it; only the source columns
9273 // (`u.bonus`) become subqueries. A whole-expr fallback covers
9274 // compound variants the leaf-walk doesn't decompose.
9275 let make_subq = |inner: Expr| {
9276 Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9277 expr: inner,
9278 alias: None,
9279 }])))
9280 };
9281 let assignments = assignments
9282 .into_iter()
9283 .map(|(col, mut expr)| {
9284 wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9285 (col, expr)
9286 })
9287 .collect();
9288 let exists = Expr::Exists {
9289 subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9290 expr: Expr::Literal(Literal::Integer(1)),
9291 alias: None,
9292 }])),
9293 negated: false,
9294 };
9295 // v7.39 (round 241) — RETURNING may reference the FROM-list
9296 // tables too (`RETURNING emp.id, dept.name`); the same
9297 // leaf-to-correlated-subquery lowering the assignments get.
9298 // Without it the qualifier died at eval with "unknown table
9299 // qualifier". (RETURNING was parsed before this block — the
9300 // lowering is a pure AST transformation.)
9301 if let Some(items) = returning.as_mut() {
9302 for item in items.iter_mut() {
9303 if let SelectItem::Expr { expr, .. } = item {
9304 wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9305 }
9306 }
9307 }
9308 // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9309 // EVERY matching target row: it gets no EXISTS filter, but the
9310 // caller's WHERE still applies, with source columns read through
9311 // the correlated subquery (NULL when unmatched — LEFT-join
9312 // semantics). `sub_where` above already excluded the WHERE from
9313 // the source subquery for this case.
9314 if mysql_outer {
9315 let mut outer = where_;
9316 if let Some(w) = outer.as_mut() {
9317 wrap_from_leaves(w, &names, &make_subq, &refs_list);
9318 }
9319 (assignments, outer)
9320 } else {
9321 (assignments, Some(exists))
9322 }
9323 } else {
9324 (assignments, where_)
9325 };
9326 Ok(Statement::Update(crate::ast::UpdateStatement {
9327 ctes: Vec::new(),
9328 table,
9329 only,
9330 alias,
9331 assignments,
9332 from_sources,
9333 where_,
9334 order_limit: update_order_limit,
9335 returning,
9336 }))
9337 }
9338
9339 /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9340 /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9341 /// clause and its meaning are identical, so both call this rather than
9342 /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9343 /// legal. PG has no such clause on either statement, so it is read only
9344 /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9345 /// errors.
9346 ///
9347 /// `#[inline(never)]`: its locals would otherwise land on the statement-
9348 /// parsing recursion frame, which is what tipped the 512 KiB nesting
9349 /// stack in round 430.
9350 #[inline(never)]
9351 fn parse_mysql_dml_order_limit(
9352 &mut self,
9353 what: &str,
9354 ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9355 if !self.mysql_dialect {
9356 return Ok(None);
9357 }
9358 let order_by = self.parse_order_by_keys()?;
9359 let limit = if matches!(self.peek(), Token::Limit) {
9360 self.advance();
9361 let tok = self.advance();
9362 let Token::Integer(n) = tok else {
9363 return Err(self.err(alloc::format!(
9364 "expected integer after {what} LIMIT, got {tok:?}"
9365 )));
9366 };
9367 // MySQL rejects the `LIMIT offset, count` form here — only a
9368 // single row count is legal on a DML statement.
9369 if matches!(self.peek(), Token::Comma) {
9370 return Err(self.err(alloc::format!(
9371 "{what} LIMIT takes a row count, not an offset"
9372 )));
9373 }
9374 let n = u32::try_from(n)
9375 .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9376 Some(n)
9377 } else {
9378 None
9379 };
9380 if order_by.is_empty() && limit.is_none() {
9381 return Ok(None);
9382 }
9383 Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9384 order_by,
9385 limit,
9386 })))
9387 }
9388
9389 /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9390 /// the leading `DELETE` ident.
9391 fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9392 // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9393 // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9394 // USING a, b WHERE …` — the third MySQL spelling — needs no special
9395 // parse here; it reaches the existing USING path with the target
9396 // repeated in the list, which the source-list peel below handles.)
9397 // More than one name is a multi-TARGET delete, which SPG does not
9398 // model; it is refused rather than half-applied.
9399 let mysql_pre_target: Option<String> =
9400 if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9401 let first = self.expect_ident_like()?;
9402 if matches!(self.peek(), Token::Comma) {
9403 return Err(self.err(alloc::format!(
9404 "multi-table DELETE can only delete from one table; \
9405 `DELETE {first}, …` names several"
9406 )));
9407 }
9408 Some(first)
9409 } else {
9410 None
9411 };
9412 if !matches!(self.peek(), Token::From) {
9413 return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9414 }
9415 self.advance();
9416 // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9417 // lookahead as the UPDATE spelling.
9418 let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9419 if s.eq_ignore_ascii_case("only"))
9420 && matches!(
9421 self.tokens.get(self.pos + 1),
9422 Some(Token::Ident(_) | Token::QuotedIdent(_))
9423 );
9424 if only {
9425 self.advance();
9426 }
9427 let table = self.expect_ident_like()?;
9428 // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9429 // spelling must not swallow the clause keywords that can follow
9430 // the target.
9431 let alias = if matches!(self.peek(), Token::As) {
9432 self.advance();
9433 Some(self.expect_ident_like()?)
9434 } else {
9435 match self.peek() {
9436 Token::Ident(s) | Token::QuotedIdent(s)
9437 if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9438 {
9439 let a = s.clone();
9440 self.advance();
9441 Some(a)
9442 }
9443 _ => None,
9444 }
9445 };
9446 // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9447 // through the SAME join grammar the FROM clause uses (see the
9448 // `advance()`-destroys-tokens note on `parse_from_joins`).
9449 let mut mysql_on: Option<Expr> = None;
9450 let mut mysql_outer = false;
9451 let mysql_using = if mysql_pre_target.is_some()
9452 && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9453 {
9454 let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9455 let mut joins = self.parse_from_joins(&target_qual)?;
9456 if joins.is_empty() {
9457 return Err(self.err(alloc::string::String::from(
9458 "multi-table DELETE needs at least one source table",
9459 )));
9460 }
9461 let head = joins.remove(0);
9462 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9463 mysql_on = head.on;
9464 Some(FromClause {
9465 primary: head.table,
9466 joins,
9467 })
9468 } else {
9469 None
9470 };
9471 // The pre-FROM target must be the table the FROM names (or its
9472 // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9473 // is not the scan target.
9474 if let Some(t) = &mysql_pre_target {
9475 let names_target = t.eq_ignore_ascii_case(&table)
9476 || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9477 if !names_target {
9478 return Err(self.err(alloc::format!(
9479 "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9480 )));
9481 }
9482 }
9483 // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9484 // delete. Same lowering as UPDATE … FROM: the WHERE
9485 // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9486 // target row by the correlated machinery.
9487 let using_clause = if let Some(fc) = mysql_using {
9488 Some(fc)
9489 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9490 self.advance();
9491 let mut fc = self.parse_from_clause()?;
9492 // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9493 // repeats the TARGET as the first USING entry (PG's spelling
9494 // lists only the extra sources). Peel it so the source subquery
9495 // does not re-scan — and shadow — the target table.
9496 let primary_is_target =
9497 fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9498 if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9499 let head = fc.joins.remove(0);
9500 mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9501 mysql_on = head.on;
9502 fc = FromClause {
9503 primary: head.table,
9504 joins: fc.joins,
9505 };
9506 }
9507 Some(fc)
9508 } else {
9509 None
9510 };
9511 let where_ = if matches!(self.peek(), Token::Where) {
9512 self.advance();
9513 Some(self.parse_expr(0)?)
9514 } else {
9515 None
9516 };
9517 // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9518 // read before RETURNING (MariaDB's own extension trails the LIMIT).
9519 let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9520 let mut returning = self.parse_optional_returning()?;
9521 let where_ = if let Some(fc) = using_clause {
9522 // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9523 // a USING-table reference in RETURNING becomes a correlated
9524 // scalar subquery over the USING list.
9525 let names: Vec<String> = core::iter::once(&fc.primary)
9526 .chain(fc.joins.iter().map(|j| &j.table))
9527 .flat_map(|t| {
9528 t.alias
9529 .clone()
9530 .into_iter()
9531 .chain(core::iter::once(t.name.clone()))
9532 })
9533 .collect();
9534 // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9535 // join filters the SOURCE subquery on the ON predicate alone and
9536 // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9537 // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9538 // rows); every other form folds ON and WHERE into one EXISTS.
9539 let sub_where = match (mysql_on.clone(), where_.clone()) {
9540 _ if mysql_outer => mysql_on.clone(),
9541 (Some(on), Some(w)) => Some(Expr::Binary {
9542 lhs: Box::new(on),
9543 op: crate::ast::BinOp::And,
9544 rhs: Box::new(w),
9545 }),
9546 (Some(on), None) => Some(on),
9547 (None, w) => w,
9548 };
9549 let exists_where = sub_where.clone();
9550 let sub_fc = fc.clone();
9551 let make_subq = move |leaf: Expr| -> Expr {
9552 Expr::ScalarSubquery(Box::new(SelectStatement {
9553 locking: None,
9554 ctes: Vec::new(),
9555 distinct: false,
9556 distinct_on: Vec::new(),
9557 items: alloc::vec![SelectItem::Expr {
9558 expr: leaf,
9559 alias: None,
9560 }],
9561 from: Some(sub_fc.clone()),
9562 where_: sub_where.clone(),
9563 group_by: None,
9564 group_by_all: false,
9565 having: None,
9566 unions: Vec::new(),
9567 order_by: Vec::new(),
9568 limit: None,
9569 offset: None,
9570 limit_with_ties: false,
9571 window_check_exprs: Vec::new(),
9572 }))
9573 };
9574 let refs = |e: &Expr| expr_refs_tables(e, &names);
9575 if let Some(items) = returning.as_mut() {
9576 for item in items.iter_mut() {
9577 if let SelectItem::Expr { expr, .. } = item {
9578 wrap_from_leaves(expr, &names, &make_subq, &refs);
9579 }
9580 }
9581 }
9582 // A LEFT join deletes the target rows the WHERE selects, reading
9583 // source columns through the correlated subquery (NULL when
9584 // unmatched); no EXISTS row filter.
9585 if mysql_outer {
9586 let mut outer = where_;
9587 if let Some(w) = outer.as_mut() {
9588 wrap_from_leaves(w, &names, &make_subq, &refs);
9589 }
9590 outer
9591 } else {
9592 Some(Expr::Exists {
9593 subquery: Box::new(SelectStatement {
9594 locking: None,
9595 ctes: Vec::new(),
9596 distinct: false,
9597 distinct_on: Vec::new(),
9598 items: alloc::vec![SelectItem::Expr {
9599 expr: Expr::Literal(Literal::Integer(1)),
9600 alias: None,
9601 }],
9602 from: Some(fc),
9603 where_: exists_where,
9604 group_by: None,
9605 group_by_all: false,
9606 having: None,
9607 unions: Vec::new(),
9608 order_by: Vec::new(),
9609 limit: None,
9610 offset: None,
9611 limit_with_ties: false,
9612 window_check_exprs: Vec::new(),
9613 }),
9614 negated: false,
9615 })
9616 }
9617 } else {
9618 where_
9619 };
9620 Ok(Statement::Delete(crate::ast::DeleteStatement {
9621 ctes: Vec::new(),
9622 table,
9623 only,
9624 alias,
9625 where_,
9626 order_limit: delete_order_limit,
9627 returning,
9628 }))
9629 }
9630
9631 /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
9632 /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
9633 /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
9634 /// keyword. v7.17 surface:
9635 /// * source: table reference (subquery source is a follow-up)
9636 /// * actions: UPDATE SET / DELETE / DO NOTHING (matched);
9637 /// INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
9638 /// * AND-conditioned WHEN clauses; clauses tried in declaration
9639 /// order
9640 fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
9641 // INTO
9642 let is_into_kw = matches!(self.peek(), Token::Into)
9643 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
9644 if !is_into_kw {
9645 return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
9646 }
9647 self.advance();
9648 let target = self.expect_ident_like()?;
9649 // Optional alias — bare ident before USING.
9650 let target_alias = match self.peek() {
9651 Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
9652 Some(self.expect_ident_like()?)
9653 }
9654 _ => None,
9655 };
9656 // USING
9657 let is_using_kw = matches!(
9658 self.peek(),
9659 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
9660 );
9661 if !is_using_kw {
9662 return Err(self.err(format!(
9663 "expected USING after MERGE INTO target, got {:?}",
9664 self.peek()
9665 )));
9666 }
9667 self.advance();
9668 // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
9669 // <table> [alias]`. PG requires an alias after a subquery source.
9670 let (source, source_select) = if matches!(self.peek(), Token::LParen) {
9671 self.advance(); // (
9672 // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
9673 // constant-SELECT lowering the derived-table parser uses
9674 // (PG deletes through this form; it was a parse error).
9675 let inner = if matches!(self.peek(), Token::Values) {
9676 self.advance(); // VALUES
9677 Statement::Select(self.parse_values_rows_body()?)
9678 } else {
9679 self.parse_select_stmt()?
9680 };
9681 match self.advance() {
9682 Token::RParen => {}
9683 other => {
9684 return Err(self.err(format!(
9685 "expected ')' after MERGE USING subquery, got {other:?}"
9686 )));
9687 }
9688 }
9689 let Statement::Select(sub) = inner else {
9690 return Err(self.err("MERGE USING subquery must be a SELECT".into()));
9691 };
9692 (String::new(), Some(Box::new(sub)))
9693 } else {
9694 (self.expect_ident_like()?, None)
9695 };
9696 let source_alias = match self.peek() {
9697 Token::Ident(s) | Token::QuotedIdent(s)
9698 if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
9699 {
9700 Some(self.expect_ident_like()?)
9701 }
9702 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
9703 self.advance(); // AS
9704 Some(self.expect_ident_like()?)
9705 }
9706 _ => None,
9707 };
9708 // v7.39 (round 768, F31-D5) — optional positional column-alias
9709 // list after the source alias (`s(id, v)`).
9710 let mut source_column_aliases: Vec<String> = Vec::new();
9711 if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
9712 self.advance();
9713 loop {
9714 source_column_aliases.push(self.expect_ident_like()?);
9715 match self.peek() {
9716 Token::Comma => {
9717 self.advance();
9718 }
9719 Token::RParen => {
9720 self.advance();
9721 break;
9722 }
9723 other => {
9724 return Err(self.err(format!(
9725 "expected ',' or ')' in MERGE source column list, got {other:?}"
9726 )));
9727 }
9728 }
9729 }
9730 }
9731 if source_select.is_some() && source_alias.is_none() {
9732 return Err(self.err("MERGE USING (subquery) requires an alias".into()));
9733 }
9734 // ON
9735 if !matches!(self.peek(), Token::On) {
9736 return Err(self.err(format!(
9737 "expected ON after MERGE … USING source, got {:?}",
9738 self.peek()
9739 )));
9740 }
9741 self.advance();
9742 let on = self.parse_expr(0)?;
9743 // One or more WHEN clauses.
9744 let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
9745 loop {
9746 let is_when_kw = matches!(
9747 self.peek(),
9748 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
9749 );
9750 if !is_when_kw {
9751 break;
9752 }
9753 self.advance(); // WHEN
9754 // [NOT] MATCHED
9755 let matched = if matches!(self.peek(), Token::Not) {
9756 self.advance();
9757 crate::ast::MergeMatched::NotMatched
9758 } else {
9759 crate::ast::MergeMatched::Matched
9760 };
9761 let is_matched_kw = matches!(
9762 self.peek(),
9763 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
9764 );
9765 if !is_matched_kw {
9766 return Err(self.err(format!(
9767 "expected MATCHED in WHEN clause, got {:?}",
9768 self.peek()
9769 )));
9770 }
9771 self.advance();
9772 // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
9773 // BY TARGET is the default (a synonym); BY SOURCE flips the clause
9774 // to fire for target rows no source row matches.
9775 let mut matched = matched;
9776 if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
9777 self.advance();
9778 match self.peek() {
9779 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
9780 self.advance();
9781 matched = crate::ast::MergeMatched::NotMatchedBySource;
9782 }
9783 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
9784 self.advance();
9785 }
9786 other => {
9787 return Err(self.err(format!(
9788 "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
9789 )));
9790 }
9791 }
9792 }
9793 // Optional AND <expr>
9794 let condition = if matches!(self.peek(), Token::And) {
9795 self.advance();
9796 Some(self.parse_expr(0)?)
9797 } else {
9798 None
9799 };
9800 // THEN
9801 let is_then_kw = matches!(
9802 self.peek(),
9803 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
9804 );
9805 if !is_then_kw {
9806 return Err(self.err(format!(
9807 "expected THEN in WHEN clause, got {:?}",
9808 self.peek()
9809 )));
9810 }
9811 self.advance();
9812 // Action: INSERT / UPDATE / DELETE / DO NOTHING
9813 let action = match self.peek().clone() {
9814 Token::Insert => {
9815 self.advance();
9816 // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
9817 // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
9818 // VALUES (…)` omits it and fills every column in declaration
9819 // order. PG accepts this; SPG used to require the list.
9820 let mut columns: Vec<String> = Vec::new();
9821 if matches!(self.peek(), Token::LParen) {
9822 self.advance();
9823 loop {
9824 columns.push(self.expect_ident_like()?);
9825 if matches!(self.peek(), Token::Comma) {
9826 self.advance();
9827 continue;
9828 }
9829 break;
9830 }
9831 if !matches!(self.peek(), Token::RParen) {
9832 return Err(self.err(format!(
9833 "expected ')' after INSERT column list, got {:?}",
9834 self.peek()
9835 )));
9836 }
9837 self.advance();
9838 }
9839 // VALUES (...)
9840 if !matches!(self.peek(), Token::Values) {
9841 return Err(self.err(format!(
9842 "expected VALUES in MERGE INSERT, got {:?}",
9843 self.peek()
9844 )));
9845 }
9846 self.advance();
9847 if !matches!(self.peek(), Token::LParen) {
9848 return Err(self.err(format!(
9849 "expected '(' after VALUES in MERGE INSERT, got {:?}",
9850 self.peek()
9851 )));
9852 }
9853 self.advance();
9854 let mut values: Vec<crate::ast::Expr> = Vec::new();
9855 loop {
9856 values.push(self.parse_expr(0)?);
9857 if matches!(self.peek(), Token::Comma) {
9858 self.advance();
9859 continue;
9860 }
9861 break;
9862 }
9863 if !matches!(self.peek(), Token::RParen) {
9864 return Err(self.err(format!(
9865 "expected ')' after MERGE INSERT values, got {:?}",
9866 self.peek()
9867 )));
9868 }
9869 self.advance();
9870 // Empty column list = positional into every column, so the
9871 // count is checked against the table arity at execution.
9872 if !columns.is_empty() && columns.len() != values.len() {
9873 return Err(self.err(format!(
9874 "MERGE INSERT column count ({}) ≠ value count ({})",
9875 columns.len(),
9876 values.len()
9877 )));
9878 }
9879 crate::ast::MergeAction::Insert { columns, values }
9880 }
9881 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
9882 self.advance();
9883 // SET
9884 let is_set_kw = matches!(
9885 self.peek(),
9886 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
9887 );
9888 if !is_set_kw {
9889 return Err(self.err(format!(
9890 "expected SET after UPDATE in MERGE, got {:?}",
9891 self.peek()
9892 )));
9893 }
9894 self.advance();
9895 let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
9896 loop {
9897 let col = self.expect_ident_like()?;
9898 if !matches!(self.peek(), Token::Eq) {
9899 return Err(self.err(format!(
9900 "expected '=' in MERGE UPDATE assignment, got {:?}",
9901 self.peek()
9902 )));
9903 }
9904 self.advance();
9905 let expr = self.parse_expr(0)?;
9906 assignments.push((col, expr));
9907 if matches!(self.peek(), Token::Comma) {
9908 self.advance();
9909 continue;
9910 }
9911 break;
9912 }
9913 crate::ast::MergeAction::Update { assignments }
9914 }
9915 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
9916 self.advance();
9917 crate::ast::MergeAction::Delete
9918 }
9919 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
9920 self.advance();
9921 let is_nothing_kw = matches!(
9922 self.peek(),
9923 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
9924 );
9925 if !is_nothing_kw {
9926 return Err(self.err(format!(
9927 "expected NOTHING after DO in MERGE clause, got {:?}",
9928 self.peek()
9929 )));
9930 }
9931 self.advance();
9932 crate::ast::MergeAction::DoNothing
9933 }
9934 other => {
9935 return Err(self.err(format!(
9936 "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
9937 )));
9938 }
9939 };
9940 // PG's grammar simply has no INSERT production under BY SOURCE
9941 // (a target row already exists there) — same syntax error.
9942 if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
9943 && matches!(action, crate::ast::MergeAction::Insert { .. })
9944 {
9945 return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
9946 }
9947 clauses.push(crate::ast::MergeWhenClause {
9948 matched,
9949 condition,
9950 action,
9951 });
9952 }
9953 if clauses.is_empty() {
9954 return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
9955 }
9956 // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
9957 // unconditional (no `AND`) WHEN of the same match kind: it could
9958 // never fire. Check per match kind in clause order.
9959 let mut seen_unconditional_matched = false;
9960 let mut seen_unconditional_not_matched = false;
9961 let mut seen_unconditional_by_source = false;
9962 for c in &clauses {
9963 let seen = match c.matched {
9964 crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
9965 crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
9966 crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
9967 };
9968 if *seen {
9969 return Err(self.err(String::from(
9970 "unreachable WHEN clause specified after unconditional WHEN clause",
9971 )));
9972 }
9973 if c.condition.is_none() {
9974 *seen = true;
9975 }
9976 }
9977 // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
9978 let returning = self.parse_optional_returning()?;
9979 Ok(Statement::Merge(crate::ast::MergeStatement {
9980 // Attached by `parse_with_cte_then_select` when the MERGE
9981 // heads a WITH clause (round 149).
9982 ctes: Vec::new(),
9983 target,
9984 target_alias,
9985 source,
9986 source_alias,
9987 source_select,
9988 source_column_aliases,
9989 on,
9990 clauses,
9991 returning,
9992 }))
9993 }
9994
9995 /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
9996 /// clause on INSERT / UPDATE / DELETE. Same projection grammar
9997 /// as SELECT, so `RETURNING *`, `RETURNING col`,
9998 /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
9999 fn parse_optional_returning(
10000 &mut self,
10001 ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10002 let is_returning_kw = matches!(
10003 self.peek(),
10004 Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10005 );
10006 if !is_returning_kw {
10007 return Ok(None);
10008 }
10009 self.advance();
10010 let mut items = Vec::new();
10011 loop {
10012 items.push(self.parse_select_item()?);
10013 if matches!(self.peek(), Token::Comma) {
10014 self.advance();
10015 continue;
10016 }
10017 break;
10018 }
10019 Ok(Some(items))
10020 }
10021
10022 /// v6.0.4 — parse the tail of an ALTER statement after the
10023 /// leading `ALTER` keyword has been consumed. Only one form is
10024 /// supported in v6.0.4:
10025 ///
10026 /// ```text
10027 /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10028 /// ```
10029 fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10030 // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10031 // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10032 // exclusion) is accepted by stripping the `ONLY` keyword
10033 // before the table parse.
10034 // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10035 // and the long PG-dump tail are accepted as no-ops.
10036 match self.advance() {
10037 Token::Index => {}
10038 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10039 // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10040 // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10041 Token::Table => {
10042 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10043 self.advance();
10044 }
10045 return self.parse_alter_table_after_keyword();
10046 }
10047 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10048 return self.parse_alter_policy_after_keyword();
10049 }
10050 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10051 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10052 self.advance();
10053 }
10054 return self.parse_alter_table_after_keyword();
10055 }
10056 // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10057 // of the silent-noop tail.
10058 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10059 return self.parse_alter_sequence_after_keyword();
10060 }
10061 // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10062 // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10063 // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10064 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10065 // NB: the match arm consumed `TYPE` via self.advance(); the
10066 // cursor is now at the type name — do NOT advance again.
10067 let type_name = self.expect_ident_like()?;
10068 let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10069 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10070 if is_add_value {
10071 self.advance(); // ADD
10072 self.advance(); // VALUE
10073 // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10074 // IF/EXISTS as identifiers.
10075 let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10076 {
10077 let n1 = self.tokens.get(self.pos + 1);
10078 let n2 = self.tokens.get(self.pos + 2);
10079 if matches!(n1, Some(Token::Not))
10080 && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10081 {
10082 self.advance();
10083 self.advance();
10084 self.advance();
10085 true
10086 } else {
10087 false
10088 }
10089 } else {
10090 false
10091 };
10092 let label = self.expect_string_literal()?;
10093 let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10094 {
10095 let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10096 self.advance();
10097 let anchor = self.expect_string_literal()?;
10098 Some((is_before, anchor))
10099 } else {
10100 None
10101 };
10102 return Ok(Statement::AlterTypeAddValue {
10103 type_name,
10104 label,
10105 if_not_exists,
10106 position,
10107 });
10108 }
10109 // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10110 // Used to fall into the no-op tail below: accepted, silently
10111 // ignored. `RENAME TO <newtype>` keeps falling through.
10112 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10113 && matches!(
10114 self.tokens.get(self.pos + 1),
10115 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10116 )
10117 {
10118 self.advance(); // RENAME
10119 self.advance(); // VALUE
10120 let old = self.expect_string_literal()?;
10121 if matches!(self.peek(), Token::To) {
10122 self.advance();
10123 } else {
10124 self.expect_keyword_ident("to")?;
10125 }
10126 let new = self.expect_string_literal()?;
10127 return Ok(Statement::AlterTypeRenameValue {
10128 type_name,
10129 old,
10130 new,
10131 });
10132 }
10133 // Other ALTER TYPE forms — the ACTION stays a no-op
10134 // (pg_dump tail), but v7.39 (round 708) the NAME is
10135 // validated: `ALTER TYPE nosuch RENAME TO x` reported
10136 // success for a type that does not exist.
10137 self.consume_until_statement_boundary();
10138 return Ok(Statement::ValidateOnly {
10139 kind: crate::ast::ValidateOnlyKind::TypeName,
10140 names: alloc::vec![type_name],
10141 });
10142 }
10143 // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10144 // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10145 // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10146 // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10147 // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10148 // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10149 // pg_dump no-op list below: every form used to report success
10150 // and change nothing, which is worse than refusing outright
10151 // (a migration dropping a constraint kept rejecting data).
10152 // NOTE: the enclosing `match self.advance()` already consumed
10153 // the DOMAIN keyword, so the name is next.
10154 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10155 return self.parse_alter_domain_after_keyword();
10156 }
10157 // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10158 // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10159 // used to fall into the pg_dump no-op tail below, so a DBA
10160 // setting a per-role default was told it worked and nothing
10161 // happened. Intercepted here, BEFORE that tail.
10162 // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10163 // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10164 // interception below exists: swallowed with the no-op tail, an
10165 // unknown parameter name was ACCEPTED where PG18 answers
10166 // `unrecognized configuration parameter`. SPG applies nothing
10167 // either way — there is no postgresql.auto.conf — but it now
10168 // says so about a name it does not know.
10169 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10170 // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10171 // already consumed here. An extra advance eats the SET and
10172 // the parameter name is never seen — which is exactly the
10173 // bug a panic in this branch disproved: the branch WAS on
10174 // the path, the reading of it was wrong.
10175 let mut parameter = None;
10176 // SET <name> … | RESET <name> | RESET ALL
10177 if matches!(self.peek(), Token::Ident(k)
10178 if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10179 {
10180 self.advance();
10181 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10182 && !n.eq_ignore_ascii_case("all")
10183 {
10184 self.advance();
10185 // A dotted GUC (`plpgsql.check_asserts`) is two
10186 // tokens; keep the whole name.
10187 let mut full = n;
10188 while matches!(self.peek(), Token::Dot) {
10189 self.advance();
10190 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10191 full.push('.');
10192 full.push_str(&t);
10193 }
10194 }
10195 parameter = Some(full);
10196 }
10197 }
10198 self.consume_until_statement_boundary();
10199 return Ok(Statement::AlterSystem { parameter });
10200 }
10201 Token::Ident(s) | Token::QuotedIdent(s)
10202 if matches!(
10203 s.to_ascii_lowercase().as_str(),
10204 "role" | "user" | "database"
10205 ) && self.peeks_db_role_setting() =>
10206 {
10207 let is_database = s.eq_ignore_ascii_case("database");
10208 return self.parse_db_role_setting(is_database);
10209 }
10210 // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10211 // (the non-SET forms; SET/RESET took the branch above). The
10212 // attributes still no-op — recorded, and the ignored PASSWORD
10213 // is ledgered as its own follow-up — but the ROLE is validated:
10214 // any name was accepted for a role that does not exist.
10215 Token::Ident(s) | Token::QuotedIdent(s)
10216 if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10217 {
10218 // NB: the enclosing `match self.advance()` already consumed
10219 // ROLE/USER — the round-695 trap, hit again in this round's
10220 // first draft (the name was eaten and WITH parsed as the
10221 // role). The cursor is at the name.
10222 let name = self.expect_ident_or_string()?;
10223 // v7.39 (round 750) — scan the attribute tail for
10224 // PASSWORD. Everything else stays a recorded no-op, but
10225 // a dropped credential rotation is a SECURITY bug:
10226 // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10227 // changed nothing, so the old password kept working.
10228 // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10229 // NULL` clears the credential.
10230 let mut password: Option<Option<String>> = None;
10231 loop {
10232 match self.peek() {
10233 Token::Semicolon | Token::Eof => break,
10234 Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10235 self.advance();
10236 match self.advance() {
10237 Token::String(p) => password = Some(Some(p)),
10238 Token::Null => password = Some(None),
10239 Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10240 password = Some(None);
10241 }
10242 other => {
10243 return Err(self.err(alloc::format!(
10244 "expected password string or NULL after PASSWORD, got {other:?}"
10245 )));
10246 }
10247 }
10248 }
10249 _ => {
10250 self.advance();
10251 }
10252 }
10253 }
10254 if name.eq_ignore_ascii_case("all") {
10255 // `ALTER ROLE ALL …` names every role; nothing to check.
10256 return Ok(Statement::Empty);
10257 }
10258 if let Some(pw) = password {
10259 return Ok(Statement::AlterRolePassword { name, password: pw });
10260 }
10261 return Ok(Statement::ValidateOnly {
10262 kind: crate::ast::ValidateOnlyKind::RoleName,
10263 names: alloc::vec![name],
10264 });
10265 }
10266 // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10267 // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10268 // list far enough to validate the NAME; the actions still no-op.
10269 // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10270 // models none of them and their dumps are rare.)
10271 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10272 let name = self.expect_ident_or_string()?;
10273 self.consume_until_statement_boundary();
10274 return Ok(Statement::ValidateOnly {
10275 kind: crate::ast::ValidateOnlyKind::CollationName,
10276 names: alloc::vec![name],
10277 });
10278 }
10279 Token::Ident(s) | Token::QuotedIdent(s)
10280 if s.eq_ignore_ascii_case("text")
10281 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10282 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10283 {
10284 self.advance(); // SEARCH
10285 self.advance(); // CONFIGURATION
10286 let name = self.expect_ident_like()?;
10287 self.consume_until_statement_boundary();
10288 return Ok(Statement::ValidateOnly {
10289 kind: crate::ast::ValidateOnlyKind::TsConfigName,
10290 names: alloc::vec![name],
10291 });
10292 }
10293 Token::Ident(s) | Token::QuotedIdent(s)
10294 if s.eq_ignore_ascii_case("event")
10295 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10296 {
10297 self.advance(); // TRIGGER
10298 let name = self.expect_ident_like()?;
10299 self.consume_until_statement_boundary();
10300 return Ok(Statement::ValidateOnly {
10301 kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10302 names: alloc::vec![name],
10303 });
10304 }
10305 Token::Ident(s) | Token::QuotedIdent(s)
10306 if s.eq_ignore_ascii_case("large")
10307 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10308 {
10309 self.advance(); // OBJECT
10310 let oid = match self.advance() {
10311 Token::Integer(n) => alloc::format!("{n}"),
10312 other => {
10313 return Err(
10314 self.err(alloc::format!("expected large object oid, got {other:?}"))
10315 );
10316 }
10317 };
10318 self.consume_until_statement_boundary();
10319 return Ok(Statement::ValidateOnly {
10320 kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10321 names: alloc::vec![oid],
10322 });
10323 }
10324 // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10325 // argument-list parse as DROP AGGREGATE (round 707); the
10326 // action no-ops, the existence check is real.
10327 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10328 // Same round-695 trap as above: AGGREGATE is already
10329 // consumed; the cursor is at the name.
10330 let name = self.expect_ident_like()?;
10331 let mut names = alloc::vec![name];
10332 if matches!(self.peek(), Token::LParen) {
10333 self.advance();
10334 loop {
10335 match self.peek().clone() {
10336 Token::RParen => {
10337 self.advance();
10338 break;
10339 }
10340 Token::Star => {
10341 self.advance();
10342 names.push(String::from("*"));
10343 }
10344 Token::Comma => {
10345 self.advance();
10346 }
10347 _ => {
10348 let mut t = self.expect_ident_like()?;
10349 while let Token::Ident(nx) = self.peek() {
10350 let nx = nx.clone();
10351 self.advance();
10352 t.push(' ');
10353 t.push_str(&nx);
10354 }
10355 names.push(t);
10356 }
10357 }
10358 }
10359 }
10360 self.consume_until_statement_boundary();
10361 return Ok(Statement::ValidateOnly {
10362 kind: crate::ast::ValidateOnlyKind::AggregateName,
10363 names,
10364 });
10365 }
10366 Token::Ident(s) | Token::QuotedIdent(s)
10367 if matches!(
10368 s.to_ascii_lowercase().as_str(),
10369 "view"
10370 | "function"
10371 | "database"
10372 | "schema"
10373 | "owner"
10374 | "default"
10375 | "extension"
10376 | "materialized"
10377 | "publication"
10378 | "subscription"
10379 // v7.37.17 (17.6 siblings) — additional ALTER
10380 // targets pg_dump / pg_dumpall / operator DB
10381 // migration scripts commonly emit. SPG has
10382 // no matching machinery for any of these; the
10383 // parser accepts + Empty-returns so pg_dump
10384 // tail statements don't stall.
10385 | "tablespace"
10386 | "language"
10387 | "operator"
10388 | "conversion"
10389 | "statistics"
10390 | "server"
10391 | "foreign"
10392 // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10393 // / TEMPLATE (CONFIGURATION intercepted above).
10394 | "text"
10395 ) =>
10396 {
10397 self.consume_until_statement_boundary();
10398 return Ok(Statement::Empty);
10399 }
10400 other => {
10401 return Err(self.err(format!(
10402 "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10403 after ALTER, got {other:?}"
10404 )));
10405 }
10406 }
10407 // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10408 // (mailrs migrate-042 ships these). The presence of an
10409 // IF EXISTS makes the subsequent name lookup tolerate
10410 // a missing index — engine returns CommandOk no-op.
10411 let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10412 let next = self.tokens.get(self.pos + 1);
10413 if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10414 self.advance();
10415 self.advance();
10416 true
10417 } else {
10418 false
10419 }
10420 } else {
10421 false
10422 };
10423 let name = self.expect_ident_like()?;
10424 // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10425 // Detect BEFORE the REBUILD path so the existing REBUILD
10426 // arm stays untouched.
10427 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10428 self.advance();
10429 if matches!(self.peek(), Token::To) {
10430 self.advance();
10431 } else {
10432 self.expect_keyword_ident("to")?;
10433 }
10434 let new = self.expect_ident_like()?;
10435 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10436 name,
10437 target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10438 }));
10439 }
10440 // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10441 // A syntax error before; the index is validated, the params no-op.
10442 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10443 || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10444 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10445 {
10446 self.consume_until_statement_boundary();
10447 return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10448 name,
10449 target: crate::ast::AlterIndexTarget::StorageParams,
10450 }));
10451 }
10452 // REBUILD
10453 self.expect_keyword_ident("rebuild")?;
10454 // Optional: WITH (encoding = <enc>)
10455 let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10456 self.advance();
10457 if !matches!(self.peek(), Token::LParen) {
10458 return Err(self.err(format!(
10459 "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10460 self.peek()
10461 )));
10462 }
10463 self.advance();
10464 self.expect_keyword_ident("encoding")?;
10465 if !matches!(self.peek(), Token::Eq) {
10466 return Err(self.err(format!(
10467 "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10468 self.peek()
10469 )));
10470 }
10471 self.advance();
10472 let enc_ident = match self.advance() {
10473 Token::Ident(s) | Token::QuotedIdent(s) => s,
10474 other => {
10475 return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10476 }
10477 };
10478 let enc = match enc_ident.to_ascii_lowercase().as_str() {
10479 "f32" => VecEncoding::F32,
10480 "sq8" => VecEncoding::Sq8,
10481 "half" => VecEncoding::F16,
10482 other => {
10483 return Err(self.err(format!(
10484 "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10485 )));
10486 }
10487 };
10488 if !matches!(self.peek(), Token::RParen) {
10489 return Err(self.err(format!(
10490 "expected ')' after encoding value, got {:?}",
10491 self.peek()
10492 )));
10493 }
10494 self.advance();
10495 Some(enc)
10496 } else {
10497 None
10498 };
10499 Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10500 name,
10501 target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10502 }))
10503 }
10504
10505 /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10506 /// only `SET` form currently supported; future v6.7.x can add
10507 /// more SET subjects without changing the dispatch shape.
10508 /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10509 /// subactions. Single-subaction shape stays a 1-element vec.
10510 fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10511 let table_name = self.expect_ident_like()?;
10512 let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10513 loop {
10514 let subaction = self.parse_alter_table_subaction()?;
10515 // ADD COLUMN with inline REFERENCES emits both an
10516 // AddColumn and an AddForeignKey subaction; the
10517 // helper returns 1 or 2 items.
10518 targets.extend(subaction);
10519 if matches!(self.peek(), Token::Comma) {
10520 self.advance();
10521 continue;
10522 }
10523 break;
10524 }
10525 Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10526 name: table_name,
10527 targets,
10528 }))
10529 }
10530
10531 /// Parse one ALTER TABLE subaction. Returns a Vec because
10532 /// inline `REFERENCES` on `ADD COLUMN` produces both an
10533 /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10534 fn parse_alter_table_subaction(
10535 &mut self,
10536 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10537 match self.peek() {
10538 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10539 self.advance();
10540 // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10541 // storage parameters: paren-prefixed; consume.
10542 if matches!(self.peek(), Token::LParen) {
10543 self.consume_until_statement_boundary();
10544 return Ok(Vec::new());
10545 }
10546 let setting = self.expect_ident_like()?;
10547 if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10548 if !matches!(self.peek(), Token::Eq) {
10549 return Err(self.err(alloc::format!(
10550 "expected '=' after hot_tier_bytes, got {:?}",
10551 self.peek()
10552 )));
10553 }
10554 self.advance();
10555 let n = self.expect_u64_literal()?;
10556 return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10557 }
10558 // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10559 // accept-and-no-op for ALTER TABLE SET <subject>
10560 // forms that pg_dump emits but SPG either treats
10561 // as N/A (single-tenant, single-owner, no shared
10562 // tablespaces) or accepts the dump-side declaration
10563 // without runtime effect:
10564 // SET SCHEMA <name> (18.11)
10565 // SET TABLESPACE <name> (18.8)
10566 // SET LOGGED / UNLOGGED (18.7 alt-form)
10567 // SET WITHOUT CLUSTER (18.13)
10568 // SET WITHOUT OIDS (PG legacy)
10569 // SET (option = value, …) (storage parameters)
10570 // SET REPLICA IDENTITY {…} (18.14)
10571 if setting.eq_ignore_ascii_case("schema")
10572 || setting.eq_ignore_ascii_case("tablespace")
10573 || setting.eq_ignore_ascii_case("logged")
10574 || setting.eq_ignore_ascii_case("unlogged")
10575 || setting.eq_ignore_ascii_case("without")
10576 {
10577 self.consume_until_statement_boundary();
10578 return Ok(Vec::new());
10579 }
10580 if setting.eq_ignore_ascii_case("replica") {
10581 // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10582 self.consume_until_statement_boundary();
10583 return Ok(Vec::new());
10584 }
10585 // SET (option=value, …) — storage parameters.
10586 if matches!(self.peek(), Token::LParen) {
10587 self.consume_until_statement_boundary();
10588 return Ok(Vec::new());
10589 }
10590 Err(self.err(alloc::format!(
10591 "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10592 hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10593 WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10594 )))
10595 }
10596 // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10597 // not ignored: round 645 gave SPG the inheritance the
10598 // v7.37.18 no-op said it did not have.
10599 Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10600 self.advance();
10601 let parent = self.expect_ident_like()?;
10602 self.consume_until_statement_boundary();
10603 Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10604 parent,
10605 detach: false
10606 }])
10607 }
10608 // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10609 // LEVEL SECURITY`, which has its own RLS arm below — without
10610 // the guard this swallowed NO FORCE as a no-op.
10611 Token::Ident(s)
10612 if s.eq_ignore_ascii_case("no")
10613 && !matches!(
10614 self.tokens.get(self.pos + 1),
10615 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10616 ) =>
10617 {
10618 self.advance();
10619 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
10620 if k.eq_ignore_ascii_case("inherit"))
10621 {
10622 self.advance();
10623 let parent = self.expect_ident_like()?;
10624 self.consume_until_statement_boundary();
10625 return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10626 parent,
10627 detach: true
10628 }]);
10629 }
10630 self.consume_until_statement_boundary();
10631 Ok(Vec::new())
10632 }
10633 // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
10634 // single-owner, so there is still nothing to record.
10635 //
10636 // v7.39 (round 652) — but the name now reaches the engine,
10637 // which refuses a role that does not exist as PG does. The
10638 // no-op was swallowing the whole statement, so a dump naming
10639 // a role this server never heard of restored clean and left
10640 // the table owned by whoever ran the restore.
10641 Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
10642 self.advance();
10643 if matches!(self.peek(), Token::To) {
10644 self.advance();
10645 }
10646 let role = self.expect_ident_like()?;
10647 Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
10648 role
10649 }])
10650 }
10651 // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
10652 // PG sets a hint; SPG doesn't have clustered storage, so the
10653 // hint itself stays a no-op.
10654 //
10655 // v7.39 (round 652) — the index name is checked now. PG
10656 // errors on one that does not exist, and swallowing that let
10657 // a typo'd CLUSTER ON pass silently.
10658 Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
10659 self.advance();
10660 // `ON` is a reserved token, not an ident.
10661 if !matches!(self.peek(), Token::On) {
10662 return Err(self.err(alloc::format!(
10663 "expected ON after CLUSTER, got {:?}",
10664 self.peek()
10665 )));
10666 }
10667 self.advance();
10668 let index = self.expect_ident_like()?;
10669 Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
10670 index: Some(index)
10671 }])
10672 }
10673 // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
10674 // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
10675 // what a logical decoder puts in the old-tuple image; SPG's
10676 // replication is SQL-text, so there is nothing to record.
10677 // Accept-and-no-op (it used to be a parse error).
10678 Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
10679 self.advance();
10680 // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
10681 // validates the index; DEFAULT / FULL / NOTHING stay no-op.
10682 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
10683 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
10684 {
10685 self.advance(); // IDENTITY
10686 self.advance(); // USING
10687 if matches!(self.peek(), Token::Index)
10688 || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
10689 {
10690 self.advance();
10691 }
10692 let index = self.expect_ident_like()?;
10693 self.consume_until_statement_boundary();
10694 return Ok(alloc::vec![
10695 crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
10696 ]);
10697 }
10698 self.consume_until_statement_boundary();
10699 Ok(Vec::new())
10700 }
10701 // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
10702 //
10703 // v7.39 (round 652) — it used to consume the statement and
10704 // return nothing, on the stated theory that SPG validated at
10705 // ADD CONSTRAINT time so there was never anything left to
10706 // validate. Measured against PG18, ADD CONSTRAINT did not
10707 // scan the existing rows at all — the comment described a
10708 // property SPG did not have, which is why nobody looked. Both
10709 // halves are real now: ADD scans unless told NOT VALID, and
10710 // this scans what NOT VALID skipped.
10711 Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
10712 self.advance();
10713 self.expect_keyword_ident("constraint")?;
10714 let name = self.expect_ident_like()?;
10715 Ok(alloc::vec![
10716 crate::ast::AlterTableTarget::ValidateConstraint { name }
10717 ])
10718 }
10719 // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
10720 // SET (option = value, …). PG uses it to clear per-table
10721 // storage params like fillfactor or autovacuum_*. SPG
10722 // engine-manages those parameters; accept-and-no-op.
10723 Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
10724 self.advance();
10725 self.consume_until_statement_boundary();
10726 Ok(Vec::new())
10727 }
10728 // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
10729 // type-of binding (PG 9.0+). SPG composite types
10730 // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
10731 // TABLE OF is rare and inverse of CREATE TABLE OF.
10732 // Accept-and-no-op until a customer dump round-trips it.
10733 Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
10734 self.advance();
10735 // v7.39 (round 710) — the type name is validated now.
10736 let type_name = self.expect_ident_like()?;
10737 self.consume_until_statement_boundary();
10738 Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
10739 type_name
10740 }])
10741 }
10742 // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
10743 // (reserved keyword) rather than Token::Ident("not"),
10744 // so it needs its own arm. Accept-and-no-op same as OF.
10745 Token::Not => {
10746 self.advance();
10747 self.consume_until_statement_boundary();
10748 Ok(Vec::new())
10749 }
10750 // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
10751 Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
10752 self.advance();
10753 self.expect_row_level_security()?;
10754 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10755 enabled: None,
10756 force: Some(true),
10757 }])
10758 }
10759 // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
10760 Token::Ident(s)
10761 if s.eq_ignore_ascii_case("no")
10762 && matches!(
10763 self.tokens.get(self.pos + 1),
10764 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
10765 ) =>
10766 {
10767 self.advance(); // NO
10768 self.advance(); // FORCE
10769 self.expect_row_level_security()?;
10770 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10771 enabled: None,
10772 force: Some(false),
10773 }])
10774 }
10775 // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
10776 // (sets relrowsecurity). The guard requires the next token to be
10777 // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
10778 Token::Ident(s)
10779 if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
10780 && matches!(
10781 self.tokens.get(self.pos + 1),
10782 Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
10783 ) =>
10784 {
10785 let enabled = s.eq_ignore_ascii_case("enable");
10786 self.advance(); // ENABLE/DISABLE
10787 self.expect_row_level_security()?;
10788 Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
10789 enabled: Some(enabled),
10790 force: None,
10791 }])
10792 }
10793 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
10794 self.advance();
10795 // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
10796 // {INDEX|KEY} [name] (cols)`, which every ORM migration
10797 // emits. The same grammar CREATE TABLE already accepts
10798 // inline (`KEY idx (a)`, prefix lengths and all), so it goes
10799 // through the SAME parser — an ALTER-only copy would be a
10800 // second place for the two to drift.
10801 if self.peek_mysql_inline_key_start() {
10802 return Ok(match self.parse_mysql_inline_key()? {
10803 Some(c) => {
10804 alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
10805 }
10806 // FULLTEXT / SPATIAL parse and are accepted as a
10807 // no-op here exactly as they are inline.
10808 None => Vec::new(),
10809 });
10810 }
10811 // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
10812 // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
10813 // PRIMARY KEY this way; mysqldump emits both.
10814 // Peek-only dispatch (no advance) — `advance()`
10815 // destructively replaces consumed tokens with Eof,
10816 // so saved-pos restore would land on Eofs.
10817 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
10818 {
10819 // The next-but-one ident is the constraint
10820 // name; the one after THAT is the kind.
10821 let kind_pos = self.pos + 2;
10822 let kind = self.tokens.get(kind_pos).cloned();
10823 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
10824 {
10825 let fk = self.parse_table_level_fk()?;
10826 return Ok(alloc::vec![
10827 crate::ast::AlterTableTarget::AddForeignKey(fk)
10828 ]);
10829 }
10830 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
10831 {
10832 self.advance(); // CONSTRAINT
10833 // v7.39 (read01 round 48) — keep the name; the engine
10834 // stores it now instead of dropping it on the floor.
10835 let con_name = self.expect_ident_like()?;
10836 self.advance(); // PRIMARY
10837 self.expect_keyword_ident("key")?;
10838 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10839 // v7.39 (round 711) — the ALTER form carries the
10840 // timing too (pg_dump writes it here).
10841 let (deferrable, initially_deferred) =
10842 self.consume_deferrable_clauses_timed()?;
10843 return Ok(alloc::vec![
10844 crate::ast::AlterTableTarget::AddTableConstraint(
10845 crate::ast::TableConstraint::PrimaryKey {
10846 name: Some(con_name),
10847 columns: cols,
10848 deferrable,
10849 initially_deferred,
10850 }
10851 )
10852 ]);
10853 }
10854 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
10855 {
10856 self.advance(); // CONSTRAINT
10857 // v7.39 (read01 round 48) — keep the name.
10858 let con_name = self.expect_ident_like()?;
10859 // v7.22 (mailrs round-13 gap 6) — delegate so
10860 // the optional `NULLS [NOT] DISTINCT` modifier
10861 // parses here too (pg_dump emits the ALTER
10862 // form; semantics enforced by the engine
10863 // since v7.13).
10864 let mut uc = self.parse_table_level_unique()?;
10865 if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
10866 *name = Some(con_name);
10867 }
10868 return Ok(alloc::vec![
10869 crate::ast::AlterTableTarget::AddTableConstraint(uc)
10870 ]);
10871 }
10872 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
10873 {
10874 self.advance(); // CONSTRAINT
10875 // v7.39 (read01 round 48) — keep the name.
10876 let con_name = self.expect_ident_like()?;
10877 self.advance(); // CHECK
10878 if !matches!(self.peek(), Token::LParen) {
10879 return Err(self.err(alloc::format!(
10880 "expected '(' after CHECK, got {:?}", self.peek()
10881 )));
10882 }
10883 self.advance();
10884 let expr = self.parse_expr(0)?;
10885 if matches!(self.peek(), Token::RParen) {
10886 self.advance();
10887 }
10888 let not_valid = self.parse_not_valid_suffix();
10889 return Ok(alloc::vec![
10890 crate::ast::AlterTableTarget::AddTableConstraint(
10891 crate::ast::TableConstraint::Check {
10892 name: Some(con_name),
10893 expr,
10894 not_valid,
10895 }
10896 )
10897 ]);
10898 }
10899 // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
10900 // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
10901 // exclusion constraints via this ALTER form.
10902 if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
10903 {
10904 self.advance(); // CONSTRAINT
10905 let con_name = self.expect_ident_like()?;
10906 let mut ex = self.parse_table_level_exclude()?;
10907 if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
10908 *name = Some(con_name);
10909 }
10910 return Ok(alloc::vec![
10911 crate::ast::AlterTableTarget::AddTableConstraint(ex)
10912 ]);
10913 }
10914 // Unknown kind — fall through to FK path which
10915 // produces a descriptive parse error.
10916 }
10917 let is_fk = matches!(
10918 self.peek(),
10919 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
10920 || s.eq_ignore_ascii_case("foreign")
10921 );
10922 if is_fk {
10923 let fk = self.parse_table_level_fk()?;
10924 return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
10925 }
10926 // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
10927 // (no CONSTRAINT prefix) — same dispatch.
10928 match self.peek().clone() {
10929 Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
10930 self.advance();
10931 self.expect_keyword_ident("key")?;
10932 let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
10933 let (deferrable, initially_deferred) =
10934 self.consume_deferrable_clauses_timed()?;
10935 return Ok(alloc::vec![
10936 crate::ast::AlterTableTarget::AddTableConstraint(
10937 crate::ast::TableConstraint::PrimaryKey {
10938 name: None,
10939 columns: cols,
10940 deferrable,
10941 initially_deferred,
10942 }
10943 )
10944 ]);
10945 }
10946 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
10947 // v7.22 — delegate (NULLS [NOT] DISTINCT).
10948 let uc = self.parse_table_level_unique()?;
10949 return Ok(alloc::vec![
10950 crate::ast::AlterTableTarget::AddTableConstraint(uc)
10951 ]);
10952 }
10953 // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
10954 // prefix). The other three bare forms were here and
10955 // this one was not, so it fell through to the column
10956 // path and came back as "unexpected reserved keyword
10957 // 'check' at start of column definition".
10958 _ if self.peek_table_level_check_start() => {
10959 let chk = self.parse_table_level_check()?;
10960 let not_valid = self.parse_not_valid_suffix();
10961 let crate::ast::TableConstraint::Check { expr, .. } = chk else {
10962 unreachable!("parse_table_level_check returns Check")
10963 };
10964 return Ok(alloc::vec![
10965 crate::ast::AlterTableTarget::AddTableConstraint(
10966 crate::ast::TableConstraint::Check {
10967 name: None,
10968 expr,
10969 not_valid,
10970 }
10971 )
10972 ]);
10973 }
10974 // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
10975 Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
10976 let ex = self.parse_table_level_exclude()?;
10977 return Ok(alloc::vec![
10978 crate::ast::AlterTableTarget::AddTableConstraint(ex)
10979 ]);
10980 }
10981 _ => {}
10982 }
10983 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
10984 self.advance();
10985 }
10986 let mut if_not_exists = false;
10987 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10988 self.advance();
10989 if !matches!(self.peek(), Token::Not) {
10990 return Err(self.err(alloc::format!(
10991 "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
10992 self.peek()
10993 )));
10994 }
10995 self.advance();
10996 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
10997 return Err(self.err(alloc::format!(
10998 "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
10999 self.peek()
11000 )));
11001 }
11002 self.advance();
11003 if_not_exists = true;
11004 }
11005 // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11006 // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11007 // returns ColumnDef + an optional inline FK.
11008 let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11009 let col_name = column.name.clone();
11010 let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11011 column,
11012 if_not_exists,
11013 }];
11014 if let Some(mut fk) = col_level_fk {
11015 if fk.columns.is_empty() {
11016 fk.columns.push(col_name);
11017 }
11018 out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11019 }
11020 Ok(out)
11021 }
11022 Token::Drop => {
11023 self.advance();
11024 // v7.13.3 — dispatch on the next token. mailrs round-7
11025 // S8 closed DROP COLUMN; round-6 S7 closed
11026 // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11027 // RESTRICT modifiers.
11028 // DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11029 // DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11030 let subject = match self.peek() {
11031 Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11032 self.advance();
11033 "constraint"
11034 }
11035 Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11036 self.advance();
11037 "column"
11038 }
11039 // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11040 // `INDEX` lexes as the reserved Token::Index, so it is
11041 // unambiguous. `KEY` is a plain ident, and PG allows a
11042 // column literally named "key", so only read it as the
11043 // keyword when a name follows it.
11044 Token::Index => {
11045 self.advance();
11046 "index"
11047 }
11048 Token::Ident(s)
11049 if s.eq_ignore_ascii_case("key")
11050 && matches!(
11051 self.tokens.get(self.pos + 1),
11052 Some(Token::Ident(_) | Token::QuotedIdent(_))
11053 ) =>
11054 {
11055 self.advance();
11056 "index"
11057 }
11058 // PG-canonical bare `DROP <col>` without COLUMN
11059 // keyword is also valid; treat any other ident
11060 // as the column name.
11061 Token::Ident(_) | Token::QuotedIdent(_) => "column",
11062 other => {
11063 return Err(self.err(alloc::format!(
11064 "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11065 )));
11066 }
11067 };
11068 let mut if_exists = false;
11069 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11070 let n1 = self.tokens.get(self.pos + 1);
11071 if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11072 self.advance();
11073 self.advance();
11074 if_exists = true;
11075 }
11076 }
11077 let name = self.expect_ident_like()?;
11078 let mut cascade = false;
11079 if matches!(
11080 self.peek(),
11081 Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11082 || s.eq_ignore_ascii_case("restrict")
11083 ) {
11084 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11085 {
11086 cascade = true;
11087 }
11088 self.advance();
11089 }
11090 if subject == "index" {
11091 Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11092 name,
11093 if_exists,
11094 }])
11095 } else if subject == "constraint" {
11096 Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11097 name,
11098 if_exists,
11099 }])
11100 } else {
11101 Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11102 column: name,
11103 if_exists,
11104 cascade,
11105 }])
11106 }
11107 }
11108 Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11109 self.advance();
11110 // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11111 // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11112 // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11113 // immediately; accept-and-no-op.
11114 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11115 self.advance();
11116 self.consume_until_statement_boundary();
11117 return Ok(Vec::new());
11118 }
11119 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11120 self.advance();
11121 }
11122 let col_name = self.expect_ident_like()?;
11123 match self.peek() {
11124 Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11125 self.advance();
11126 }
11127 // v7.14.0 — pg_dump emits BIGSERIAL via
11128 // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11129 // nextval('seq')` (the sequence is created
11130 // separately). SPG's BIGSERIAL already uses
11131 // AUTO_INCREMENT; accept SET DEFAULT / DROP
11132 // DEFAULT / SET NOT NULL / DROP NOT NULL as
11133 // engine no-ops by consuming the tail.
11134 Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11135 // v7.22 (round-13 T2) — `SET DEFAULT
11136 // nextval('…')` is how pg_dump spells a
11137 // SERIAL column (plain integer in CREATE
11138 // TABLE + this ALTER). It used to be
11139 // swallowed as a no-op, which silently
11140 // STRIPPED auto-increment from imported
11141 // schemas — the first post-import INSERT
11142 // without an explicit id then violated NOT
11143 // NULL. Lower it to the auto-increment
11144 // marker instead.
11145 let is_default_nextval =
11146 matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11147 && matches!(
11148 self.tokens.get(self.pos + 2),
11149 Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11150 );
11151 if is_default_nextval {
11152 let seq_name = self.scan_sequence_name_until_boundary();
11153 return Ok(alloc::vec![
11154 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11155 column: col_name,
11156 seq_name,
11157 }
11158 ]);
11159 }
11160 // v7.37.18 (18.1 + 18.2) — proper lowering.
11161 self.advance(); // consume "set"
11162 match self.peek().clone() {
11163 Token::Default => {
11164 self.advance();
11165 let default_expr = self.parse_expr(0)?;
11166 return Ok(alloc::vec![
11167 crate::ast::AlterTableTarget::AlterColumnSetDefault {
11168 column: col_name,
11169 default_expr,
11170 }
11171 ]);
11172 }
11173 Token::Not => {
11174 self.advance();
11175 if !matches!(self.peek(), Token::Null) {
11176 return Err(self.err(alloc::format!(
11177 "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11178 self.peek()
11179 )));
11180 }
11181 self.advance();
11182 return Ok(alloc::vec![
11183 crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11184 column: col_name,
11185 }
11186 ]);
11187 }
11188 // `SET EXPRESSION AS (expr)` (PG 17) — change a
11189 // stored generated column's expression and
11190 // recompute existing rows.
11191 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11192 self.advance(); // EXPRESSION
11193 if matches!(self.peek(), Token::As) {
11194 self.advance();
11195 }
11196 let expr = self.parse_expr(0)?;
11197 return Ok(alloc::vec![
11198 crate::ast::AlterTableTarget::AlterColumnSetExpression {
11199 column: col_name,
11200 expr,
11201 }
11202 ]);
11203 }
11204 other => {
11205 // Other SET subjects (STATISTICS,
11206 // STORAGE, COMPRESSION, …) stay no-ops —
11207 // storage hints with no SPG semantics.
11208 let _ = other;
11209 self.consume_until_statement_boundary();
11210 return Ok(Vec::new());
11211 }
11212 }
11213 }
11214 Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11215 self.advance(); // consume "drop"
11216 return self.parse_alter_column_drop_tail(col_name);
11217 }
11218 Token::Drop => {
11219 self.advance(); // consume Drop token
11220 return self.parse_alter_column_drop_tail(col_name);
11221 }
11222 Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11223 // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11224 // GENERATED { ALWAYS | BY DEFAULT } AS
11225 // IDENTITY ( … )`: pg_dump's spelling for
11226 // identity columns. Same auto-increment
11227 // lowering as the nextval default; the
11228 // sequence options inside the parens are
11229 // no-ops under SPG's max+1 semantics.
11230 let is_generated = matches!(
11231 self.tokens.get(self.pos + 1),
11232 Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11233 );
11234 if !is_generated {
11235 return Err(self.err(alloc::format!(
11236 "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11237 self.tokens.get(self.pos + 1)
11238 )));
11239 }
11240 let seq_name = self.scan_sequence_name_until_boundary();
11241 return Ok(alloc::vec![
11242 crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11243 column: col_name,
11244 seq_name,
11245 }
11246 ]);
11247 }
11248 // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11249 // column: floor the next allocated value at n (bare
11250 // RESTART = restart from the start value, 1).
11251 Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11252 self.advance();
11253 let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11254 {
11255 self.advance();
11256 let neg = if matches!(self.peek(), Token::Minus) {
11257 self.advance();
11258 true
11259 } else {
11260 false
11261 };
11262 match self.advance() {
11263 Token::Integer(v) => Some(if neg { -v } else { v }),
11264 other => {
11265 return Err(self.err(alloc::format!(
11266 "expected integer after RESTART WITH, got {other:?}"
11267 )));
11268 }
11269 }
11270 } else {
11271 None
11272 };
11273 return Ok(alloc::vec![
11274 crate::ast::AlterTableTarget::AlterColumnRestart {
11275 column: col_name,
11276 with,
11277 }
11278 ]);
11279 }
11280 other => {
11281 return Err(self.err(alloc::format!(
11282 "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11283 )));
11284 }
11285 }
11286 // v7.39 (round 713) — the type parser has consumed a
11287 // trailing `COLLATE <name>` since Phase 2.5, and
11288 // `parse_column_type_name` discarded it: `ALTER COLUMN t
11289 // TYPE text COLLATE "C"` parsed clean and changed
11290 // nothing. Keep the clause; the engine re-collates.
11291 let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _) =
11292 self.parse_type_with_implied_flags()?;
11293 let collation = if coll_explicit {
11294 coll_name.map(|n| (coll, n))
11295 } else {
11296 None
11297 };
11298 let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11299 {
11300 self.advance();
11301 Some(self.parse_expr(0)?)
11302 } else {
11303 None
11304 };
11305 Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11306 column: col_name,
11307 new_type,
11308 using,
11309 collation,
11310 }])
11311 }
11312 // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11313 // PG also supports `RENAME TO new_table` for table-name
11314 // rename; that surface is deferred (pg_dump never emits
11315 // it). If the first post-RENAME ident is `TO`, the user
11316 // is asking for table rename — error with a clear
11317 // message rather than misparsing `TO` as a column name.
11318 Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11319 self.advance();
11320 // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11321 // table-name rename (mailrs round-10 A.5 — used
11322 // by migrate-042's `RENAME TO email_contacts`).
11323 // `TO` lexes as Token::To.
11324 if matches!(self.peek(), Token::To)
11325 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11326 {
11327 self.advance();
11328 let new = self.expect_ident_like()?;
11329 return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11330 new,
11331 }]);
11332 }
11333 // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11334 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11335 self.advance();
11336 let old = self.expect_ident_like()?;
11337 if matches!(self.peek(), Token::To) {
11338 self.advance();
11339 } else {
11340 self.expect_keyword_ident("to")?;
11341 }
11342 let new = self.expect_ident_like()?;
11343 return Ok(alloc::vec![
11344 crate::ast::AlterTableTarget::RenameConstraint { old, new }
11345 ]);
11346 }
11347 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11348 self.advance();
11349 }
11350 let old = self.expect_ident_like()?;
11351 // `TO` is a reserved keyword token; accept both
11352 // Token::To and Token::Ident("to") for consistency.
11353 if matches!(self.peek(), Token::To) {
11354 self.advance();
11355 } else {
11356 self.expect_keyword_ident("to")?;
11357 }
11358 let new = self.expect_ident_like()?;
11359 Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11360 old,
11361 new,
11362 }])
11363 }
11364 // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11365 // { ALL | <name> }`. pg_dump --disable-triggers wraps
11366 // every data block with these. Real disable semantics —
11367 // not no-op — because reload correctness assumes the
11368 // triggers don't fire (rows already carry their
11369 // computed values from prod).
11370 Token::Ident(s)
11371 if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11372 {
11373 let enabled = s.eq_ignore_ascii_case("enable");
11374 self.advance();
11375 // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11376 // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11377 // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11378 // pg_dump output) — anything else falls through to
11379 // the catch-all error below.
11380 // v7.22 (round-13 T3) — mysqldump wraps every data
11381 // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11382 // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11383 // maintains indexes incrementally — engine no-op.
11384 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11385 self.advance();
11386 return Ok(Vec::new());
11387 }
11388 // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11389 // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11390 // to gate triggers on session_replication_role; SPG
11391 // has no replica role, so the prefix is consumed and
11392 // treated identically to the plain ENABLE/DISABLE
11393 // TRIGGER form.
11394 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11395 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11396 {
11397 self.advance();
11398 }
11399 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11400 return Err(self.err(alloc::format!(
11401 "expected TRIGGER after {}, got {:?}",
11402 if enabled { "ENABLE" } else { "DISABLE" },
11403 self.peek()
11404 )));
11405 }
11406 self.advance();
11407 // `ALL` lexes as Token::All (reserved); also
11408 // accept Token::Ident("all") for symmetry.
11409 // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11410 // TRIGGER selectors. USER (= all user triggers) is
11411 // semantically ALL here; REPLICA / ALWAYS gate on
11412 // session_replication_role which SPG doesn't track.
11413 // All map to TriggerSelector::All.
11414 let which = if matches!(self.peek(), Token::All)
11415 || matches!(self.peek(), Token::Ident(s)
11416 if s.eq_ignore_ascii_case("all")
11417 || s.eq_ignore_ascii_case("user")
11418 || s.eq_ignore_ascii_case("replica")
11419 || s.eq_ignore_ascii_case("always"))
11420 {
11421 self.advance();
11422 crate::ast::TriggerSelector::All
11423 } else {
11424 let name = self.expect_ident_like()?;
11425 crate::ast::TriggerSelector::Named(name)
11426 };
11427 Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11428 which,
11429 enabled,
11430 }])
11431 }
11432 // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11433 Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11434 self.advance();
11435 if !matches!(self.peek(), Token::Partition)
11436 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11437 if s.eq_ignore_ascii_case("partition"))
11438 {
11439 return Err(self.err(alloc::format!(
11440 "expected PARTITION after ATTACH, got {:?}",
11441 self.peek()
11442 )));
11443 }
11444 self.advance();
11445 let child = self.expect_ident_like()?;
11446 let bounds = self.parse_partition_bounds_tail()?;
11447 Ok(alloc::vec![
11448 crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11449 ])
11450 }
11451 // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11452 Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11453 self.advance();
11454 if !matches!(self.peek(), Token::Partition)
11455 && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11456 if s.eq_ignore_ascii_case("partition"))
11457 {
11458 return Err(self.err(alloc::format!(
11459 "expected PARTITION after DETACH, got {:?}",
11460 self.peek()
11461 )));
11462 }
11463 self.advance();
11464 let child = self.expect_ident_like()?;
11465 let mut concurrently = false;
11466 let mut finalize = false;
11467 loop {
11468 match self.peek().clone() {
11469 Token::Ident(s) | Token::QuotedIdent(s)
11470 if s.eq_ignore_ascii_case("concurrently") =>
11471 {
11472 self.advance();
11473 concurrently = true;
11474 }
11475 Token::Ident(s) | Token::QuotedIdent(s)
11476 if s.eq_ignore_ascii_case("finalize") =>
11477 {
11478 self.advance();
11479 finalize = true;
11480 }
11481 _ => break,
11482 }
11483 }
11484 Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11485 child,
11486 concurrently,
11487 finalize,
11488 }])
11489 }
11490 other => Err(self.err(alloc::format!(
11491 "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11492 ))),
11493 }
11494 }
11495
11496 /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11497 /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11498 /// TABLE … ATTACH PARTITION. Shares the same grammar as
11499 /// `parse_partition_of_tail`'s bounds branch.
11500 /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11501 /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11502 /// lowering each to the respective AlterTableTarget. Any
11503 /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11504 /// no-op via consume_until_statement_boundary.
11505 fn parse_alter_column_drop_tail(
11506 &mut self,
11507 col_name: String,
11508 ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11509 match self.peek().clone() {
11510 Token::Default => {
11511 self.advance();
11512 Ok(alloc::vec![
11513 crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11514 ])
11515 }
11516 Token::Not => {
11517 self.advance();
11518 if !matches!(self.peek(), Token::Null) {
11519 return Err(self.err(alloc::format!(
11520 "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11521 self.peek()
11522 )));
11523 }
11524 self.advance();
11525 Ok(alloc::vec![
11526 crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11527 ])
11528 }
11529 // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11530 // generated column into a plain column.
11531 Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11532 self.advance();
11533 // v7.39 (round 187, U10) — IF EXISTS was consumed but
11534 // dropped, so the engine still errored on a plain
11535 // column; PG's semantics are NOTICE + skip.
11536 let mut if_exists = false;
11537 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11538 self.advance();
11539 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11540 self.advance();
11541 if_exists = true;
11542 }
11543 }
11544 Ok(alloc::vec![
11545 crate::ast::AlterTableTarget::AlterColumnDropExpression {
11546 column: col_name,
11547 if_exists,
11548 }
11549 ])
11550 }
11551 // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11552 // identity column into a plain column.
11553 Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11554 self.advance();
11555 let mut if_exists = false;
11556 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11557 self.advance();
11558 if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11559 self.advance();
11560 if_exists = true;
11561 }
11562 }
11563 Ok(alloc::vec![
11564 crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11565 column: col_name,
11566 if_exists,
11567 }
11568 ])
11569 }
11570 _ => {
11571 self.consume_until_statement_boundary();
11572 Ok(Vec::new())
11573 }
11574 }
11575 }
11576
11577 /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11578 /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11579 /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11580 /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11581 fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11582 let mut opts = crate::ast::CopyOptions::default();
11583 if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11584 return Ok(opts);
11585 }
11586 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11587 self.advance();
11588 }
11589 if matches!(self.peek(), Token::LParen) {
11590 self.advance();
11591 loop {
11592 self.parse_one_copy_option(&mut opts)?;
11593 match self.peek() {
11594 Token::Comma => {
11595 self.advance();
11596 }
11597 Token::RParen => {
11598 self.advance();
11599 break;
11600 }
11601 other => {
11602 return Err(self.err(alloc::format!(
11603 "expected ',' or ')' in COPY options, got {other:?}"
11604 )));
11605 }
11606 }
11607 }
11608 } else {
11609 while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11610 self.parse_one_copy_option(&mut opts)?;
11611 }
11612 }
11613 if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11614 return Err(self.err(alloc::format!(
11615 "unexpected token after COPY options: {:?}",
11616 self.peek()
11617 )));
11618 }
11619 Ok(opts)
11620 }
11621
11622 fn parse_one_copy_option(
11623 &mut self,
11624 opts: &mut crate::ast::CopyOptions,
11625 ) -> Result<(), ParseError> {
11626 use crate::ast::CopyFormat;
11627 // The option keyword. NULL lexes as its own token; the rest are
11628 // bare identifiers.
11629 let kw = match self.advance() {
11630 Token::Null => alloc::string::String::from("NULL"),
11631 Token::Ident(s) => s.to_uppercase(),
11632 other => {
11633 return Err(self.err(alloc::format!(
11634 "expected a COPY option keyword, got {other:?}"
11635 )));
11636 }
11637 };
11638 match kw.as_str() {
11639 "FORMAT" => {
11640 let fmt = self.expect_ident_like()?;
11641 match fmt.to_ascii_uppercase().as_str() {
11642 "CSV" => opts.format = CopyFormat::Csv,
11643 "TEXT" => opts.format = CopyFormat::Text,
11644 other => {
11645 return Err(self.err(alloc::format!(
11646 "COPY format \"{}\" not recognized",
11647 other.to_ascii_lowercase()
11648 )));
11649 }
11650 }
11651 }
11652 // Legacy bare format keywords.
11653 "CSV" => opts.format = CopyFormat::Csv,
11654 "TEXT" => opts.format = CopyFormat::Text,
11655 "HEADER" => {
11656 opts.header = match self.peek() {
11657 Token::True => {
11658 self.advance();
11659 true
11660 }
11661 Token::False => {
11662 self.advance();
11663 false
11664 }
11665 Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
11666 self.advance();
11667 true
11668 }
11669 Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
11670 self.advance();
11671 false
11672 }
11673 // Bare HEADER (no boolean) means HEADER true.
11674 _ => true,
11675 };
11676 }
11677 "DELIMITER" | "QUOTE" | "ESCAPE" => {
11678 let s = match self.advance() {
11679 Token::String(s) => s,
11680 other => {
11681 return Err(self.err(alloc::format!(
11682 "COPY {kw} expects a single-character string, got {other:?}"
11683 )));
11684 }
11685 };
11686 // v7.39 (round 247) — PG's wording (0A000), keyword in
11687 // lowercase: "COPY delimiter must be a single one-byte
11688 // character".
11689 let one_byte_err = || {
11690 self.err(alloc::format!(
11691 "COPY {} must be a single one-byte character",
11692 kw.to_ascii_lowercase()
11693 ))
11694 };
11695 let mut chars = s.chars();
11696 let c = chars.next().ok_or_else(one_byte_err)?;
11697 if chars.next().is_some() || c.len_utf8() != 1 {
11698 return Err(one_byte_err());
11699 }
11700 match kw.as_str() {
11701 "DELIMITER" => opts.delimiter = Some(c),
11702 "QUOTE" => opts.quote = Some(c),
11703 _ => opts.escape = Some(c),
11704 }
11705 }
11706 // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
11707 "FORCE_QUOTE" => {
11708 if matches!(self.peek(), Token::Star) {
11709 self.advance();
11710 opts.force_quote = Some(Vec::new());
11711 } else {
11712 if !matches!(self.peek(), Token::LParen) {
11713 return Err(self.err(alloc::format!(
11714 "expected '(' or '*' after FORCE_QUOTE, got {:?}",
11715 self.peek()
11716 )));
11717 }
11718 self.advance();
11719 let mut cols = Vec::new();
11720 loop {
11721 cols.push(self.expect_ident_like()?);
11722 match self.peek() {
11723 Token::Comma => {
11724 self.advance();
11725 }
11726 Token::RParen => {
11727 self.advance();
11728 break;
11729 }
11730 other => {
11731 return Err(self.err(alloc::format!(
11732 "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
11733 )));
11734 }
11735 }
11736 }
11737 opts.force_quote = Some(cols);
11738 }
11739 }
11740 "NULL" => {
11741 opts.null_str = Some(match self.advance() {
11742 Token::String(s) => s,
11743 other => {
11744 return Err(self.err(alloc::format!(
11745 "COPY NULL expects a quoted string, got {other:?}"
11746 )));
11747 }
11748 });
11749 }
11750 // v7.39 (round 265) — the two CSV FROM-side column lists. Same
11751 // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
11752 // FORCE_NULL too.
11753 "FORCE_NOT_NULL" | "FORCE_NULL" => {
11754 let cols = self.parse_copy_column_list(&kw)?;
11755 if kw == "FORCE_NOT_NULL" {
11756 opts.force_not_null = Some(cols);
11757 } else {
11758 opts.force_null = Some(cols);
11759 }
11760 }
11761 other => {
11762 // PG's wording, lowercased option name.
11763 return Err(self.err(alloc::format!(
11764 "option \"{}\" not recognized",
11765 other.to_ascii_lowercase()
11766 )));
11767 }
11768 }
11769 Ok(())
11770 }
11771
11772 /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
11773 /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
11774 /// is the `*` spelling.
11775 fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
11776 if matches!(self.peek(), Token::Star) {
11777 self.advance();
11778 return Ok(Vec::new());
11779 }
11780 if !matches!(self.peek(), Token::LParen) {
11781 return Err(self.err(alloc::format!(
11782 "expected '(' or '*' after {kw}, got {:?}",
11783 self.peek()
11784 )));
11785 }
11786 self.advance();
11787 let mut cols = Vec::new();
11788 loop {
11789 cols.push(self.expect_ident_like()?);
11790 match self.peek() {
11791 Token::Comma => {
11792 self.advance();
11793 }
11794 Token::RParen => {
11795 self.advance();
11796 break;
11797 }
11798 other => {
11799 return Err(self.err(alloc::format!(
11800 "expected ',' or ')' in {kw} list, got {other:?}"
11801 )));
11802 }
11803 }
11804 }
11805 Ok(cols)
11806 }
11807
11808 fn parse_partition_bounds_tail(
11809 &mut self,
11810 ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
11811 use crate::ast::PartitionOfBoundsAst;
11812 match self.peek() {
11813 Token::Default => {
11814 self.advance();
11815 Ok(PartitionOfBoundsAst::Default)
11816 }
11817 Token::For => {
11818 self.advance();
11819 if !matches!(self.peek(), Token::Values) {
11820 return Err(
11821 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
11822 );
11823 }
11824 self.advance();
11825 let want_with = matches!(
11826 self.peek(),
11827 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
11828 );
11829 if want_with {
11830 self.advance();
11831 if !matches!(self.peek(), Token::LParen) {
11832 return Err(self.err(format!(
11833 "expected '(' after FOR VALUES WITH, got {:?}",
11834 self.peek()
11835 )));
11836 }
11837 self.advance();
11838 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
11839 loop {
11840 let key = self.expect_ident_like()?;
11841 let n = match self.peek().clone() {
11842 Token::Integer(v) if u32::try_from(v).is_ok() => {
11843 self.advance();
11844 v as u32
11845 }
11846 other => {
11847 return Err(self.err(format!(
11848 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
11849 )));
11850 }
11851 };
11852 match key.to_ascii_uppercase().as_str() {
11853 "MODULUS" => modulus = Some(n),
11854 "REMAINDER" => remainder = Some(n),
11855 other => {
11856 return Err(self.err(format!(
11857 "FOR VALUES WITH: unknown key {other:?}; \
11858 expected MODULUS or REMAINDER"
11859 )));
11860 }
11861 }
11862 match self.peek() {
11863 Token::Comma => {
11864 self.advance();
11865 }
11866 Token::RParen => {
11867 self.advance();
11868 break;
11869 }
11870 other => {
11871 return Err(self.err(format!(
11872 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
11873 )));
11874 }
11875 }
11876 }
11877 let modulus = modulus
11878 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
11879 let remainder = remainder.ok_or_else(|| {
11880 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
11881 })?;
11882 if modulus == 0 {
11883 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
11884 }
11885 if remainder >= modulus {
11886 return Err(self.err(format!(
11887 "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
11888 )));
11889 }
11890 return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
11891 }
11892 match self.peek() {
11893 Token::From => {
11894 self.advance();
11895 let lower = Box::new(self.parse_partition_bound_expr()?);
11896 if !matches!(self.peek(), Token::To) {
11897 return Err(self.err(format!(
11898 "expected TO after FROM (...), got {:?}",
11899 self.peek()
11900 )));
11901 }
11902 self.advance();
11903 let upper = Box::new(self.parse_partition_bound_expr()?);
11904 Ok(PartitionOfBoundsAst::Range { lower, upper })
11905 }
11906 Token::In => {
11907 self.advance();
11908 if !matches!(self.peek(), Token::LParen) {
11909 return Err(self.err(format!(
11910 "expected '(' after FOR VALUES IN, got {:?}",
11911 self.peek()
11912 )));
11913 }
11914 self.advance();
11915 let mut values = Vec::new();
11916 loop {
11917 values.push(self.parse_expr(0)?);
11918 match self.peek() {
11919 Token::Comma => {
11920 self.advance();
11921 }
11922 Token::RParen => {
11923 self.advance();
11924 break;
11925 }
11926 other => {
11927 return Err(self.err(format!(
11928 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
11929 )));
11930 }
11931 }
11932 }
11933 if values.is_empty() {
11934 return Err(
11935 self.err("FOR VALUES IN requires at least one literal".to_string())
11936 );
11937 }
11938 Ok(PartitionOfBoundsAst::List { values })
11939 }
11940 other => Err(self.err(format!(
11941 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
11942 ))),
11943 }
11944 }
11945 other => Err(self.err(format!(
11946 "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
11947 ))),
11948 }
11949 }
11950
11951 /// v7.16.2 — peek for `information_schema.<tbl>` /
11952 /// `pg_catalog.<tbl>` triples and, if matched, consume all
11953 /// three tokens + return a synthetic table name the engine's
11954 /// SELECT path recognises as a virtual view. Returns `None`
11955 /// when the head doesn't look like a meta-qualified name.
11956 /// Used by `parse_table_ref` to bypass the
11957 /// `expect_ident_like` schema-strip for these specific PG
11958 /// meta schemas (mailrs round-10 A.3).
11959 fn try_peek_meta_qualified(&mut self) -> Option<String> {
11960 // Extract the schema name. Must be a plain ident token.
11961 let schema = match self.tokens.get(self.pos) {
11962 Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
11963 _ => return None,
11964 };
11965 // Dot.
11966 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
11967 return None;
11968 }
11969 // The table-side ident may lex as a reserved keyword
11970 // (e.g. `Token::Tables`). Tolerate the common ones via a
11971 // helper that reads the trailing token's underlying name.
11972 let tbl = match self.tokens.get(self.pos + 2)? {
11973 Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
11974 Token::Tables => "tables".to_string(),
11975 // Other PG meta table names that may collide with
11976 // reserved keywords land here as needed.
11977 _ => return None,
11978 };
11979 // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
11980 // names so the synthetic name doesn't double-prefix
11981 // (`__spg_pg_class`, not `__spg_pg_pg_class`).
11982 let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
11983 ("__spg_info_", tbl.to_ascii_lowercase())
11984 } else if schema.eq_ignore_ascii_case("pg_catalog") {
11985 // v7.39 (round 541) — only the catalogs SPG actually
11986 // synthesises are rewritten, which is what the BARE path
11987 // has always checked. Anything else keeps its own name and
11988 // takes the ordinary route: `pg_stat_activity` and friends
11989 // resolve through meta_view_result, and a name that is no
11990 // catalog at all gets PG's "relation does not exist"
11991 // instead of a message about a view SPG cannot materialise.
11992 let lowered = tbl.to_ascii_lowercase();
11993 if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
11994 self.advance(); // schema
11995 self.advance(); // dot
11996 self.advance(); // tbl
11997 return Some(lowered);
11998 }
11999 let bare = lowered
12000 .strip_prefix("pg_")
12001 .map(alloc::string::String::from)
12002 .unwrap_or(lowered);
12003 ("__spg_pg_", bare)
12004 } else if schema.eq_ignore_ascii_case("mysql") {
12005 // v7.17.0 Phase 3.P0-65 — MySQL system schema
12006 // (`mysql.user`, `mysql.db`). Same synthetic-name
12007 // shape as pg_catalog.
12008 ("__spg_mysql_", tbl.to_ascii_lowercase())
12009 } else {
12010 return None;
12011 };
12012 self.advance(); // schema
12013 self.advance(); // dot
12014 self.advance(); // tbl
12015 Some(alloc::format!("{prefix}{normalised}"))
12016 }
12017
12018 /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12019 /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12020 /// implicit front of every search_path, so a bare reference to a
12021 /// known catalog table always means the catalog table. Only the
12022 /// names the engine actually synthesises are recognised — any
12023 /// other `pg_*` ident stays a user table (mailrs embed round-12).
12024 fn try_peek_meta_bare(&mut self) -> Option<String> {
12025 // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12026 // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12027 // `pg_catalog` at the front of every search_path. (pg_stat_activity
12028 // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12029 // through the meta_view_result path instead, and already resolve
12030 // bare — they must NOT be listed here or the __spg_ rewrite would
12031 // mis-target them.)
12032 const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12033 let name = match self.tokens.get(self.pos) {
12034 Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12035 _ => return None,
12036 };
12037 // A following dot means this ident is a schema qualifier,
12038 // not a table name — let the qualified path handle it.
12039 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12040 return None;
12041 }
12042 if !PG_META_TABLES.contains(&name.as_str()) {
12043 return None;
12044 }
12045 self.advance();
12046 let bare = name.strip_prefix("pg_").unwrap_or(&name);
12047 Some(alloc::format!("__spg_pg_{bare}"))
12048 }
12049
12050 /// Consume a bare ident if its lowercase matches `kw`, else err.
12051 /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12052 /// Peeks only; the caller advances.
12053 fn peek_keyword_ident(&self, kw: &str) -> bool {
12054 matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12055 }
12056
12057 fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12058 match self.advance() {
12059 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12060 other => Err(ParseError {
12061 message: format!("expected {kw:?}, got {other:?}"),
12062 token_pos: self.consumed_pos(),
12063 }),
12064 }
12065 }
12066
12067 /// Accept either a quoted identifier (`"foo"`) or a quoted string
12068 /// literal (`'foo'`) — same shape used by CREATE USER for the
12069 /// username slot.
12070 fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12071 match self.advance() {
12072 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12073 other => Err(ParseError {
12074 message: format!("expected identifier or string, got {other:?}"),
12075 token_pos: self.consumed_pos(),
12076 }),
12077 }
12078 }
12079
12080 fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12081 match self.advance() {
12082 Token::String(s) => Ok(s),
12083 other => Err(ParseError {
12084 message: format!("expected quoted string, got {other:?}"),
12085 token_pos: self.consumed_pos(),
12086 }),
12087 }
12088 }
12089
12090 fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12091 // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12092 // subqueries recurse through here without passing
12093 // parse_expr; share the same nesting budget.
12094 self.enter_nested()?;
12095 let r = self.parse_select_stmt_inner();
12096 self.nest_depth -= 1;
12097 r
12098 }
12099
12100 fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12101 // Caller dispatches on Token::Select; the inner helper handles
12102 // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12103 // get a fresh bare-select parse and may not have their own ORDER
12104 // BY / LIMIT.
12105 let mut head = self.parse_bare_select()?;
12106 self.parse_setop_chain_into(&mut head)?;
12107 self.parse_select_tail_into(&mut head)?;
12108 Ok(Statement::Select(head))
12109 }
12110
12111 /// v7.37.17 (17.6 siblings) — the three SQL set operations
12112 /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12113 /// token), and INTERSECT [ALL] (a bare ident — it was never
12114 /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12115 /// tighter than UNION / EXCEPT — the executor folds the chain
12116 /// left-to-right, which is already correct for LEADING
12117 /// intersects; an INTERSECT pair that FOLLOWS a union/except
12118 /// pair nests into that previous peer, so A UNION B INTERSECT C
12119 /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12120 /// groups.
12121 fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12122 // A parenthesized group arrives with its own (already
12123 // regrouped) unions on `head`; only the pairs THIS chain
12124 // appends participate in the precedence regroup below —
12125 // nesting an outer INTERSECT into a group-internal peer
12126 // would dissolve the explicit grouping.
12127 let boundary = head.unions.len();
12128 loop {
12129 let base = match self.peek() {
12130 Token::Union => UnionKind::Distinct,
12131 Token::Except => UnionKind::Except,
12132 Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12133 _ => break,
12134 };
12135 self.advance();
12136 let kind = if matches!(self.peek(), Token::All) {
12137 self.advance();
12138 match base {
12139 UnionKind::Distinct => UnionKind::All,
12140 UnionKind::Except => UnionKind::ExceptAll,
12141 _ => UnionKind::IntersectAll,
12142 }
12143 } else {
12144 base
12145 };
12146 let peer = self.parse_bare_select()?;
12147 head.unions.push((kind, peer));
12148 }
12149 let mut pairs = core::mem::take(&mut head.unions);
12150 let tail = pairs.split_off(boundary);
12151 let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12152 for (kind, peer) in tail {
12153 let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12154 // An intersect nests into the previous element of THIS
12155 // chain only; with no new previous element it stays at
12156 // the outer level (the left fold applies it to the
12157 // whole head, group included).
12158 match (
12159 is_intersect,
12160 regrouped.len() > boundary,
12161 regrouped.last_mut(),
12162 ) {
12163 (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12164 _ => regrouped.push((kind, peer)),
12165 }
12166 }
12167 head.unions = regrouped;
12168 Ok(())
12169 }
12170
12171 /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12172 /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12173 /// the top-level bare VALUES statement reuses it verbatim.
12174 /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12175 /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12176 /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12177 /// where the grouping-set universe is still in scope.
12178 fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12179 if !matches!(self.peek(), Token::Order) {
12180 return Ok(Vec::new());
12181 }
12182 self.advance();
12183 if !self.peek_is_by() {
12184 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12185 }
12186 self.advance();
12187 let mut keys = Vec::new();
12188 loop {
12189 // v7.39 (round 691) — save/restore, the discipline this parser
12190 // already uses around `pending_sample_preds`, so a subquery inside
12191 // a key neither inherits nor leaks the channel.
12192 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12193 let saved_coll = self.order_key_collation.take();
12194 let parsed = self.parse_expr(0);
12195 self.in_order_by_key = saved_flag;
12196 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12197 let expr = parsed?;
12198 let desc = if matches!(self.peek(), Token::Desc) {
12199 self.advance();
12200 true
12201 } else if matches!(self.peek(), Token::Asc) {
12202 self.advance();
12203 false
12204 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12205 // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12206 // one ordering per type, so the btree comparison operators map
12207 // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12208 // would need a custom operator class — honest error.
12209 self.advance();
12210 match self.advance() {
12211 Token::Lt | Token::LtEq => false,
12212 Token::Gt | Token::GtEq => true,
12213 other => {
12214 return Err(self.err(alloc::format!(
12215 "ORDER BY USING supports the btree comparison \
12216 operators (< <= > >=); got {other:?}"
12217 )));
12218 }
12219 }
12220 } else {
12221 false
12222 };
12223 // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12224 let nulls_first = self.parse_optional_nulls_placement()?;
12225 keys.push(OrderBy {
12226 expr,
12227 desc,
12228 nulls_first,
12229 collation,
12230 });
12231 if matches!(self.peek(), Token::Comma) {
12232 self.advance();
12233 } else {
12234 break;
12235 }
12236 }
12237 Ok(keys)
12238 }
12239
12240 fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12241 // v7.39 (round 135) — a grouping-set query may have already parsed +
12242 // rewritten its ORDER BY (to reference synthetic grouping columns); if
12243 // no ORDER BY token is present, keep that pre-set order_by rather than
12244 // clobbering it with an empty list.
12245 let parsed_keys = self.parse_order_by_keys()?;
12246 head.order_by = if parsed_keys.is_empty() {
12247 core::mem::take(&mut head.order_by)
12248 } else {
12249 parsed_keys
12250 };
12251 // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12252 // order. PG's grammar takes a limit clause and an offset clause
12253 // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12254 // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12255 // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12256 // spelling died on `expected end of input, got Limit`.
12257 //
12258 // Each may appear at most once, and LIMIT and FETCH FIRST are
12259 // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12260 // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12261 // A second one is left unconsumed here, which the caller reports
12262 // as trailing input rather than silently taking the last.
12263 let mut saw_limit = false;
12264 let mut saw_offset = false;
12265 loop {
12266 if !saw_limit && matches!(self.peek(), Token::Limit) {
12267 self.advance();
12268 // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12269 // PG synonyms for "no limit". Treat both as None
12270 // (no head.limit set) so the engine's existing
12271 // unlimited-result path takes over. Reject was the
12272 // pre-5.1 behaviour and broke pg_dump-flavoured
12273 // tooling that occasionally emits LIMIT NULL.
12274 if self.consume_limit_unbounded_sentinel() {
12275 head.limit = None;
12276 } else {
12277 let first = self.parse_limit_expr("LIMIT")?;
12278 // MySQL `LIMIT offset, count` — the first number is
12279 // the offset when a comma follows.
12280 if matches!(self.peek(), Token::Comma) {
12281 self.advance();
12282 let count = self.parse_limit_expr("LIMIT")?;
12283 head.offset = Some(first);
12284 saw_offset = true;
12285 head.limit = Some(count);
12286 } else {
12287 head.limit = Some(first);
12288 }
12289 }
12290 saw_limit = true;
12291 continue;
12292 }
12293 if !saw_offset && matches!(self.peek(), Token::Offset) {
12294 self.advance();
12295 // PG also accepts an optional `ROW` / `ROWS` trailer
12296 // after the offset value (`OFFSET 10 ROWS`). The
12297 // FETCH-FIRST branch below relies on the same.
12298 let off = self.parse_limit_expr("OFFSET")?;
12299 self.consume_optional_rows_keyword();
12300 head.offset = Some(off);
12301 saw_offset = true;
12302 continue;
12303 }
12304 // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12305 // the SQL-standard alias for LIMIT. PG accepts both
12306 // spellings interchangeably; pg_dump emits FETCH FIRST in
12307 // newer versions. We map it onto `head.limit` so the
12308 // engine path is unified.
12309 if !saw_limit
12310 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12311 if s.eq_ignore_ascii_case("fetch"))
12312 {
12313 self.advance(); // FETCH
12314 // `FIRST` or `NEXT` (both legal per SQL standard).
12315 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12316 if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12317 {
12318 self.advance();
12319 }
12320 // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12321 // implicit 1 — but we always consume one if present).
12322 let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12323 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12324 {
12325 // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12326 crate::ast::LimitExpr::Literal(1)
12327 } else {
12328 self.parse_limit_expr("FETCH FIRST")?
12329 };
12330 // Eat `ROW` / `ROWS` if not already consumed above.
12331 self.consume_optional_rows_keyword();
12332 // Optional `ONLY` (the spec form) — or the SQL:2008
12333 // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12334 // now honours WITH TIES by extending past the LIMIT
12335 // truncation point through every row that shares the
12336 // last-kept row's ORDER BY key.
12337 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12338 if s.eq_ignore_ascii_case("only"))
12339 {
12340 self.advance();
12341 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12342 if s.eq_ignore_ascii_case("with"))
12343 {
12344 self.advance(); // WITH
12345 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12346 if s.eq_ignore_ascii_case("ties"))
12347 {
12348 self.advance();
12349 head.limit_with_ties = true;
12350 }
12351 }
12352 head.limit = Some(count);
12353 saw_limit = true;
12354 continue;
12355 }
12356 break;
12357 }
12358 // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12359 // FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12360 // [ OF table_name [, …] ]
12361 // [ NOWAIT | SKIP LOCKED ]
12362 // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12363 // FOR SHARE OF t2`). SPG is a single-writer engine — every
12364 // SELECT already returns a consistent snapshot — so these
12365 // are accept-and-discard: the parser absorbs them so
12366 // mailrs / Rails / Django code paths that emit `SELECT
12367 // … FOR UPDATE` for advisory pessimistic locking load
12368 // without a parser error. The on-disk locking model is
12369 // unchanged; callers that rely on FOR UPDATE for read-
12370 // through-write ordering still get the right answer
12371 // because SPG serialises writes anyway.
12372 head.locking = self
12373 .consume_optional_for_lock_clauses()
12374 .map(alloc::boxed::Box::new);
12375 Ok(())
12376 }
12377
12378 /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12379 /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12380 /// LOCKED ]` trailers. Each clause is fully accepted and
12381 /// discarded — SPG's single-writer model already satisfies the
12382 /// callers' implicit ordering requirement. Stops at the first
12383 /// token that isn't `FOR`.
12384 fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12385 // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12386 // not discarded. PG keeps the strongest of several clauses; the
12387 // policy of the last one wins, which is what this loop records.
12388 let mut seen: Option<crate::ast::LockingClause> = None;
12389 while matches!(self.peek(), Token::For) {
12390 // v7.37.14 (A2.5-stub) — record that this query asked
12391 // for a row lock the parser is about to silently
12392 // discard. Operators surface the count via
12393 // `spg_sql::silent_for_update_count()` so they can
12394 // gauge how much of the workload depends on advisory
12395 // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12396 // before v7.37.15's per-row tuple locking lands.
12397 crate::record_silent_for_update_clause();
12398 self.advance(); // FOR
12399 // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12400 // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12401 let mut no_key = false;
12402 let mut key = false;
12403 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12404 if s.eq_ignore_ascii_case("no"))
12405 {
12406 self.advance(); // NO
12407 no_key = true;
12408 // The next ident should be KEY but be generous;
12409 // anything followed by UPDATE/SHARE is accepted.
12410 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12411 if s.eq_ignore_ascii_case("key"))
12412 {
12413 self.advance(); // KEY
12414 }
12415 }
12416 // `KEY` prefix (PG `FOR KEY SHARE`).
12417 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12418 if s.eq_ignore_ascii_case("key"))
12419 {
12420 self.advance(); // KEY
12421 key = true;
12422 }
12423 // Lock-strength keyword: UPDATE / SHARE. Required, but
12424 // we're lenient — an unexpected token here just bails
12425 // (we already consumed FOR; caller's downstream
12426 // dispatch will error if anything actually depends on
12427 // the trailing tokens).
12428 let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12429 if s.eq_ignore_ascii_case("update"));
12430 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12431 if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12432 {
12433 self.advance();
12434 use crate::ast::LockStrength as LS;
12435 let strength = match (is_update, no_key, key) {
12436 (true, true, _) => LS::NoKeyUpdate,
12437 (true, _, _) => LS::Update,
12438 (false, _, true) => LS::KeyShare,
12439 (false, _, _) => LS::Share,
12440 };
12441 seen = Some(crate::ast::LockingClause {
12442 strength,
12443 of_tables: alloc::vec::Vec::new(),
12444 policy: crate::ast::LockWait::Wait,
12445 });
12446 } else {
12447 // FOR by itself (or `FOR KEY` with nothing after) —
12448 // give up on the lock-clause path. We've already
12449 // advanced past FOR; further attempts to parse
12450 // here would clobber state.
12451 return seen;
12452 }
12453 // Optional `OF tbl[, tbl …]`. mailrs emits this when
12454 // joining and locking only a subset of tables.
12455 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12456 if s.eq_ignore_ascii_case("of"))
12457 {
12458 self.advance(); // OF
12459 #[allow(clippy::while_let_loop)]
12460 loop {
12461 match self.peek() {
12462 Token::Ident(_) | Token::QuotedIdent(_) => {
12463 // v7.39 (round 294) — the name is CAPTURED now: PG
12464 // validates it against the FROM clause, and an
12465 // uncaptured list silently means "lock everything".
12466 let mut nm = match self.advance() {
12467 Token::Ident(n) | Token::QuotedIdent(n) => n,
12468 _ => alloc::string::String::new(),
12469 };
12470 // Optional schema-qualified `schema.table`.
12471 if matches!(self.peek(), Token::Dot) {
12472 self.advance();
12473 if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12474 {
12475 self.advance();
12476 nm = n;
12477 }
12478 }
12479 if let Some(c) = seen.as_mut() {
12480 c.of_tables.push(nm);
12481 }
12482 }
12483 _ => break,
12484 }
12485 if matches!(self.peek(), Token::Comma) {
12486 self.advance();
12487 } else {
12488 break;
12489 }
12490 }
12491 }
12492 // Optional `NOWAIT` | `SKIP LOCKED`.
12493 match self.peek().clone() {
12494 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12495 self.advance();
12496 if let Some(c) = seen.as_mut() {
12497 c.policy = crate::ast::LockWait::NoWait;
12498 }
12499 }
12500 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12501 self.advance(); // SKIP
12502 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12503 if s.eq_ignore_ascii_case("locked"))
12504 {
12505 self.advance(); // LOCKED
12506 if let Some(c) = seen.as_mut() {
12507 c.policy = crate::ast::LockWait::SkipLocked;
12508 }
12509 }
12510 }
12511 _ => {}
12512 }
12513 // Loop: PG allows multiple FOR clauses chained.
12514 }
12515 seen
12516 }
12517
12518 /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12519 /// Bind value gets resolved during prepared-statement Execute;
12520 /// the Pratt expression parser would over-accept here (e.g.
12521 /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12522 /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12523 /// sentinel tokens (PG synonyms for "no limit"). Returns true
12524 /// when one was consumed; caller skips the regular
12525 /// limit-value parse and leaves `head.limit` at None.
12526 fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12527 if matches!(self.peek(), Token::Null) {
12528 self.advance();
12529 return true;
12530 }
12531 if matches!(self.peek(), Token::All) {
12532 self.advance();
12533 return true;
12534 }
12535 false
12536 }
12537
12538 /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12539 /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12540 /// SQL-standard shape. No-op when missing.
12541 fn consume_optional_rows_keyword(&mut self) {
12542 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12543 if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12544 {
12545 self.advance();
12546 }
12547 }
12548
12549 /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12550 ///
12551 /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12552 /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12553 /// constant, which is why that spelling keeps the token path below.
12554 ///
12555 /// Constants are folded here rather than carried into the tree: the
12556 /// 15+ execution paths that read the row count go through
12557 /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12558 /// means "no limit". A clause the engine could not resolve would
12559 /// therefore return the WHOLE table instead of failing. Folding at
12560 /// parse time keeps that impossible; a non-constant clause is still
12561 /// a clean error (recorded residual — closing it wants a resolution
12562 /// pre-pass on the simple-query path, where `substitute_placeholders`
12563 /// does not run).
12564 fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12565 // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12566 // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12567 // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12568 // ONLY` both work (its grammar takes a c_expr). Both measured
12569 // against PG 18.4 in round 305.
12570 if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12571 return self.parse_limit_constant(label);
12572 }
12573 // One pass, no rewind: `advance()` takes each token by
12574 // `mem::replace`, so a consumed token reads back as Eof and this
12575 // parser cannot backtrack. Everything — bare literal included —
12576 // is therefore folded from the parsed expression rather than
12577 // re-read from the token stream.
12578 let start = self.pos;
12579 let e = self.parse_expr(0)?;
12580 if let crate::ast::Expr::Placeholder(n) = e {
12581 return Ok(crate::ast::LimitExpr::Placeholder(n));
12582 }
12583 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12584 match fold_limit_constant(&e) {
12585 Some(Ok(v)) if v < 0 => Err(ParseError {
12586 message: alloc::format!("{neg_label} must not be negative"),
12587 token_pos: start,
12588 }),
12589 Some(Ok(v)) => u32::try_from(v)
12590 .map(crate::ast::LimitExpr::Literal)
12591 .map_err(|_| ParseError {
12592 message: alloc::format!("{label} value too large: {v}"),
12593 token_pos: start,
12594 }),
12595 Some(Err(message)) => Err(ParseError {
12596 message: message.replace("{L}", neg_label),
12597 token_pos: start,
12598 }),
12599 // v7.39 (round 305, V23) — not foldable at parse time
12600 // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
12601 // expression; the engine evaluates it once before dispatch.
12602 None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
12603 }
12604 }
12605
12606 fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12607 // v7.39 (round 239) — PG's row-count clause takes a bigint with its
12608 // coercion rules, not just an integer token: a NUMERIC rounds half
12609 // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
12610 // refused with PG's wording ("LIMIT must not be negative", 2201W /
12611 // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
12612 // content, failing as an input-syntax error on the value. General
12613 // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
12614 // they need an Expr-carrying LimitExpr variant.
12615 let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
12616 let err_at = |message: alloc::string::String, pos: usize| ParseError {
12617 message,
12618 token_pos: pos,
12619 };
12620 match self.advance() {
12621 Token::Integer(n) if n >= 0 => u32::try_from(n)
12622 .map(crate::ast::LimitExpr::Literal)
12623 .map_err(|_| ParseError {
12624 message: alloc::format!("{label} value too large: {n}"),
12625 token_pos: self.consumed_pos(),
12626 }),
12627 Token::Integer(_) => Err(err_at(
12628 alloc::format!("{neg_label} must not be negative"),
12629 self.pos.saturating_sub(1),
12630 )),
12631 Token::Numeric(t) => {
12632 let pos = self.pos.saturating_sub(1);
12633 let v: f64 = t.parse().map_err(|_| {
12634 err_at(
12635 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12636 pos,
12637 )
12638 })?;
12639 if v < 0.0 {
12640 return Err(err_at(
12641 alloc::format!("{neg_label} must not be negative"),
12642 pos,
12643 ));
12644 }
12645 // Round half away from zero — PG's numeric→bigint cast.
12646 // (no_std: no f64::round; v is non-negative, so truncating
12647 // v + 0.5 is the same thing.)
12648 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12649 let rounded = (v + 0.5) as u64;
12650 u32::try_from(rounded)
12651 .map(crate::ast::LimitExpr::Literal)
12652 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
12653 }
12654 Token::Minus => {
12655 let pos = self.pos.saturating_sub(1);
12656 match self.peek() {
12657 Token::Integer(_) | Token::Numeric(_) => {
12658 self.advance();
12659 Err(err_at(
12660 alloc::format!("{neg_label} must not be negative"),
12661 pos,
12662 ))
12663 }
12664 other => Err(err_at(
12665 alloc::format!(
12666 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12667 ),
12668 pos,
12669 )),
12670 }
12671 }
12672 Token::String(t) => {
12673 let pos = self.pos.saturating_sub(1);
12674 match t.trim().parse::<i64>() {
12675 Ok(n) if n < 0 => Err(err_at(
12676 alloc::format!("{neg_label} must not be negative"),
12677 pos,
12678 )),
12679 Ok(n) => u32::try_from(n)
12680 .map(crate::ast::LimitExpr::Literal)
12681 .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
12682 Err(_) => Err(err_at(
12683 alloc::format!("invalid input syntax for type bigint: \"{t}\""),
12684 pos,
12685 )),
12686 }
12687 }
12688 Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
12689 other => Err(ParseError {
12690 message: alloc::format!(
12691 "expected non-negative integer or $N placeholder after {label}, got {other:?}"
12692 ),
12693 token_pos: self.consumed_pos(),
12694 }),
12695 }
12696 }
12697
12698 /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
12699 /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
12700 /// `unions` empty and `order_by` / `limit` `None`; the top-level
12701 /// `parse_select_stmt` is responsible for filling those in.
12702 /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
12703 /// call in the expression tree to the per-set integer bitmask
12704 /// (PG semantics: one bit per argument, MSB first; 1 = the key
12705 /// is dropped in this grouping set). Runs during the ROLLUP /
12706 /// CUBE / GROUPING SETS expansion, where the set is known.
12707 /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
12708 /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
12709 fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
12710 if let Expr::FunctionCall { name, .. } = expr
12711 && name.eq_ignore_ascii_case("grouping")
12712 {
12713 if !out.iter().any(|e| e == expr) {
12714 out.push(expr.clone());
12715 }
12716 return;
12717 }
12718 match expr {
12719 Expr::Binary { lhs, rhs, .. } => {
12720 Self::collect_grouping_calls(lhs, out);
12721 Self::collect_grouping_calls(rhs, out);
12722 }
12723 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12724 Self::collect_grouping_calls(expr, out)
12725 }
12726 Expr::FunctionCall { args, .. } => {
12727 for a in args {
12728 Self::collect_grouping_calls(a, out);
12729 }
12730 }
12731 Expr::Case {
12732 operand,
12733 branches,
12734 else_branch,
12735 } => {
12736 if let Some(o) = operand {
12737 Self::collect_grouping_calls(o, out);
12738 }
12739 for (c, v) in branches {
12740 Self::collect_grouping_calls(c, out);
12741 Self::collect_grouping_calls(v, out);
12742 }
12743 if let Some(x) = else_branch {
12744 Self::collect_grouping_calls(x, out);
12745 }
12746 }
12747 _ => {}
12748 }
12749 }
12750
12751 /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
12752 /// `grp_exprs[k]` with a reference to the synthetic ordering column
12753 /// `__grp_ord_k` (injected per grouping-set branch).
12754 fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
12755 if let Expr::FunctionCall { name, .. } = expr
12756 && name.eq_ignore_ascii_case("grouping")
12757 {
12758 if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
12759 *expr = Expr::Column(crate::ast::ColumnName {
12760 qualifier: None,
12761 name: alloc::format!("__grp_ord_{k}"),
12762 });
12763 }
12764 return;
12765 }
12766 match expr {
12767 Expr::Binary { lhs, rhs, .. } => {
12768 Self::rewrite_grouping_to_col(lhs, grp_exprs);
12769 Self::rewrite_grouping_to_col(rhs, grp_exprs);
12770 }
12771 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
12772 Self::rewrite_grouping_to_col(expr, grp_exprs)
12773 }
12774 Expr::FunctionCall { args, .. } => {
12775 for a in args {
12776 Self::rewrite_grouping_to_col(a, grp_exprs);
12777 }
12778 }
12779 Expr::Case {
12780 operand,
12781 branches,
12782 else_branch,
12783 } => {
12784 if let Some(o) = operand {
12785 Self::rewrite_grouping_to_col(o, grp_exprs);
12786 }
12787 for (c, v) in branches {
12788 Self::rewrite_grouping_to_col(c, grp_exprs);
12789 Self::rewrite_grouping_to_col(v, grp_exprs);
12790 }
12791 if let Some(x) = else_branch {
12792 Self::rewrite_grouping_to_col(x, grp_exprs);
12793 }
12794 }
12795 _ => {}
12796 }
12797 }
12798
12799 /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
12800 /// as the list of key sets it contributes. A bare expression is one
12801 /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
12802 /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
12803 /// the concatenation of its items' sets, where an item is itself an
12804 /// element, a parenthesized key list, or the empty set `()`. A
12805 /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
12806 /// move together.
12807 fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
12808 let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
12809 // ROLLUP ( … ) / CUBE ( … )
12810 if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
12811 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
12812 {
12813 let is_cube = is_kw(self.peek(), "cube");
12814 self.advance(); // ROLLUP / CUBE
12815 self.advance(); // (
12816 let mut units: Vec<Vec<Expr>> = Vec::new();
12817 loop {
12818 if matches!(self.peek(), Token::LParen) {
12819 // Composite unit: (a, b) rolls up as one.
12820 self.advance();
12821 let mut unit = Vec::new();
12822 if !matches!(self.peek(), Token::RParen) {
12823 loop {
12824 unit.push(self.parse_expr(0)?);
12825 match self.peek() {
12826 Token::Comma => {
12827 self.advance();
12828 }
12829 Token::RParen => break,
12830 other => {
12831 return Err(self.err(format!(
12832 "expected ',' or ')' in grouping unit, got {other:?}"
12833 )));
12834 }
12835 }
12836 }
12837 }
12838 self.advance(); // )
12839 units.push(unit);
12840 } else {
12841 units.push(alloc::vec![self.parse_expr(0)?]);
12842 }
12843 match self.peek() {
12844 Token::Comma => {
12845 self.advance();
12846 }
12847 Token::RParen => break,
12848 other => {
12849 return Err(self.err(format!(
12850 "expected ',' or ')' in grouping list, got {other:?}"
12851 )));
12852 }
12853 }
12854 }
12855 self.advance(); // )
12856 let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
12857 units
12858 .iter()
12859 .zip(unit_sel.iter())
12860 .filter(|(_, keep)| **keep)
12861 .flat_map(|(u, _)| u.iter().cloned())
12862 .collect()
12863 };
12864 let n = units.len();
12865 if is_cube {
12866 let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
12867 .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
12868 .collect();
12869 subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
12870 return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
12871 }
12872 return Ok((0..=n)
12873 .rev()
12874 .map(|keep| {
12875 let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
12876 flatten(&sel)
12877 })
12878 .collect());
12879 }
12880 // GROUPING SETS ( item [, item]* )
12881 if is_kw(self.peek(), "grouping")
12882 && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
12883 {
12884 self.advance(); // GROUPING
12885 self.advance(); // SETS
12886 if !matches!(self.peek(), Token::LParen) {
12887 return Err(self.err(format!(
12888 "expected '(' after GROUPING SETS, got {:?}",
12889 self.peek()
12890 )));
12891 }
12892 self.advance(); // outer (
12893 let mut sets: Vec<Vec<Expr>> = Vec::new();
12894 loop {
12895 if matches!(self.peek(), Token::LParen) {
12896 // A parenthesized key list (or the empty set).
12897 self.advance();
12898 let mut set = Vec::new();
12899 if !matches!(self.peek(), Token::RParen) {
12900 loop {
12901 set.push(self.parse_expr(0)?);
12902 match self.peek() {
12903 Token::Comma => {
12904 self.advance();
12905 }
12906 Token::RParen => break,
12907 other => {
12908 return Err(self.err(format!(
12909 "expected ',' or ')' in grouping set, got {other:?}"
12910 )));
12911 }
12912 }
12913 }
12914 }
12915 self.advance(); // )
12916 sets.push(set);
12917 } else {
12918 // A nested element: ROLLUP/CUBE/GROUPING SETS or a
12919 // bare expression.
12920 sets.extend(self.parse_grouping_element()?);
12921 }
12922 match self.peek() {
12923 Token::Comma => {
12924 self.advance();
12925 }
12926 Token::RParen => break,
12927 other => {
12928 return Err(self.err(format!(
12929 "expected ',' or ')' after a grouping set, got {other:?}"
12930 )));
12931 }
12932 }
12933 }
12934 self.advance(); // outer )
12935 return Ok(sets);
12936 }
12937 Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
12938 }
12939
12940 fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
12941 // v7.38 (read01) — a reference to a key that is dropped in this grouping
12942 // set evaluates to NULL, at any depth. Previously only a *top-level*
12943 // select item equal to a dropped key was nullified, so a key nested in
12944 // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
12945 // column and failed to resolve against the set's synthetic schema.
12946 if dropped.iter().any(|d| d == expr) {
12947 *expr = Expr::Literal(Literal::Null);
12948 return;
12949 }
12950 if let Expr::FunctionCall { name, args } = expr
12951 && name.eq_ignore_ascii_case("grouping")
12952 {
12953 let mut mask: i64 = 0;
12954 for a in args.iter() {
12955 mask <<= 1;
12956 if dropped.iter().any(|d| d == a) {
12957 mask |= 1;
12958 }
12959 }
12960 // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
12961 // literal: a bare integer in a select item is indistinguishable
12962 // from a positional reference once `ORDER BY 1` substitutes the
12963 // item back in, and the round-232 position check then read the
12964 // mask value as an out-of-range position. The cast changes
12965 // nothing semantically (grouping() is integer).
12966 *expr = Expr::Cast {
12967 expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
12968 target: crate::ast::CastTarget::Int,
12969 };
12970 return;
12971 }
12972 // Generic recursion over the common expression shapes the
12973 // SELECT list uses; anything without child expressions is
12974 // left alone.
12975 match expr {
12976 Expr::FunctionCall { args, .. } => {
12977 for a in args {
12978 Self::substitute_grouping_calls(a, dropped);
12979 }
12980 }
12981 Expr::Binary { lhs, rhs, .. } => {
12982 Self::substitute_grouping_calls(lhs, dropped);
12983 Self::substitute_grouping_calls(rhs, dropped);
12984 }
12985 Expr::Unary { expr: inner, .. } => {
12986 Self::substitute_grouping_calls(inner, dropped);
12987 }
12988 Expr::Cast { expr: inner, .. } => {
12989 Self::substitute_grouping_calls(inner, dropped);
12990 }
12991 Expr::Case {
12992 operand,
12993 branches,
12994 else_branch,
12995 } => {
12996 if let Some(op) = operand {
12997 Self::substitute_grouping_calls(op, dropped);
12998 }
12999 for (w, t) in branches {
13000 Self::substitute_grouping_calls(w, dropped);
13001 Self::substitute_grouping_calls(t, dropped);
13002 }
13003 if let Some(e) = else_branch {
13004 Self::substitute_grouping_calls(e, dropped);
13005 }
13006 }
13007 // v7.38 (read01) — recurse into the remaining child-bearing shapes
13008 // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13009 // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13010 // …` is the canonical rollup-total label idiom).
13011 Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13012 Expr::Like { expr, pattern, .. } => {
13013 Self::substitute_grouping_calls(expr, dropped);
13014 Self::substitute_grouping_calls(pattern, dropped);
13015 }
13016 Expr::InList { expr, list, .. } => {
13017 Self::substitute_grouping_calls(expr, dropped);
13018 for item in list {
13019 Self::substitute_grouping_calls(item, dropped);
13020 }
13021 }
13022 Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13023 Expr::Array(items) => {
13024 for item in items {
13025 Self::substitute_grouping_calls(item, dropped);
13026 }
13027 }
13028 Expr::ArraySubscript { target, index } => {
13029 Self::substitute_grouping_calls(target, dropped);
13030 Self::substitute_grouping_calls(index, dropped);
13031 }
13032 Expr::ArraySlice { target, lo, hi } => {
13033 Self::substitute_grouping_calls(target, dropped);
13034 if let Some(lo) = lo {
13035 Self::substitute_grouping_calls(lo, dropped);
13036 }
13037 if let Some(hi) = hi {
13038 Self::substitute_grouping_calls(hi, dropped);
13039 }
13040 }
13041 Expr::AnyAll { expr, array, .. } => {
13042 Self::substitute_grouping_calls(expr, dropped);
13043 Self::substitute_grouping_calls(array, dropped);
13044 }
13045 _ => {}
13046 }
13047 }
13048
13049 fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13050 // v7.37.17 (17.6 siblings) — parenthesized set-operation
13051 // group: `( <select chain> )` usable anywhere a query block
13052 // is (head or peer of an outer chain). The group's own
13053 // unions ride the returned SelectStatement; the executor's
13054 // nested-peer recursion runs them.
13055 if matches!(self.peek(), Token::LParen)
13056 && matches!(
13057 self.tokens.get(self.pos + 1),
13058 Some(Token::Select | Token::LParen | Token::Values)
13059 )
13060 {
13061 self.advance(); // (
13062 self.enter_nested()?;
13063 // v7.37 D.20 — a group whose head is a VALUES list:
13064 // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13065 // otherwise recurse into a nested SELECT/group head.
13066 let mut head = (if matches!(self.peek(), Token::Values) {
13067 self.advance(); // VALUES
13068 self.parse_values_rows_body()
13069 } else {
13070 self.parse_bare_select()
13071 })
13072 .and_then(|mut h| {
13073 self.parse_setop_chain_into(&mut h)?;
13074 Ok(h)
13075 });
13076 self.nest_depth -= 1;
13077 let mut head = match &mut head {
13078 Ok(h) => core::mem::take(h),
13079 Err(_) => return head,
13080 };
13081 // v7.37.17 (17.6 siblings) — group-internal tail:
13082 // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13083 // group head, then wrap the group as a derived table
13084 // (SELECT * FROM (group)) so the outer chain / outer
13085 // tail can't clobber the group's own ordering or limit.
13086 let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13087 || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13088 if s.eq_ignore_ascii_case("fetch"));
13089 if has_tail {
13090 self.parse_select_tail_into(&mut head)?;
13091 head = SelectStatement {
13092 locking: None,
13093 ctes: Vec::new(),
13094 distinct: false,
13095 distinct_on: Vec::new(),
13096 items: alloc::vec![SelectItem::Wildcard],
13097 from: Some(FromClause {
13098 primary: TableRef {
13099 name: "subquery".to_string(),
13100 alias: None,
13101 only: false,
13102 as_of_segment: None,
13103 unnest_expr: None,
13104 unnest_column_aliases: Vec::new(),
13105 with_ordinality: false,
13106 generate_series_args: None,
13107 lateral_subquery: Some(Box::new(head)),
13108 jsonb_each_text_arg: None,
13109 table_fn_call: None,
13110 rows_from: None,
13111 json_table: None,
13112 scalar_fn_item: false,
13113 },
13114 joins: Vec::new(),
13115 }),
13116 where_: None,
13117 group_by: None,
13118 group_by_all: false,
13119 having: None,
13120 unions: Vec::new(),
13121 order_by: Vec::new(),
13122 limit: None,
13123 offset: None,
13124 limit_with_ties: false,
13125 window_check_exprs: Vec::new(),
13126 };
13127 }
13128 if !matches!(self.peek(), Token::RParen) {
13129 return Err(self.err(format!(
13130 "expected ')' after parenthesized query group, got {:?}",
13131 self.peek()
13132 )));
13133 }
13134 self.advance();
13135 return Ok(head);
13136 }
13137 // `TABLE name` shorthand as a query block — valid anywhere
13138 // a SELECT head is (set-op peers included).
13139 if matches!(self.peek(), Token::Table)
13140 && matches!(
13141 self.tokens.get(self.pos + 1),
13142 Some(Token::Ident(_) | Token::QuotedIdent(_))
13143 )
13144 {
13145 return self.parse_table_shorthand();
13146 }
13147 if !matches!(self.peek(), Token::Select) {
13148 return Err(self.err(format!(
13149 "expected SELECT to start a query block, got {:?}",
13150 self.peek()
13151 )));
13152 }
13153 self.advance();
13154 let distinct = if matches!(self.peek(), Token::Distinct) {
13155 self.advance();
13156 true
13157 } else {
13158 false
13159 };
13160 // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13161 // keep the first row (per ORDER BY) of each group the
13162 // expressions define. Django's .distinct('field') shape.
13163 let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13164 self.advance(); // ON
13165 if !matches!(self.peek(), Token::LParen) {
13166 return Err(self.err(format!(
13167 "expected '(' after DISTINCT ON, got {:?}",
13168 self.peek()
13169 )));
13170 }
13171 self.advance();
13172 let mut exprs = Vec::new();
13173 loop {
13174 exprs.push(self.parse_expr(0)?);
13175 match self.peek() {
13176 Token::Comma => {
13177 self.advance();
13178 }
13179 Token::RParen => break,
13180 other => {
13181 return Err(self.err(format!(
13182 "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13183 )));
13184 }
13185 }
13186 }
13187 self.advance(); // )
13188 exprs
13189 } else {
13190 Vec::new()
13191 };
13192 let mut items = self.parse_select_list()?;
13193 // Scope the TABLESAMPLE lowering channel to this SELECT:
13194 // stash whatever an enclosing select accumulated, collect
13195 // our own FROM's predicates, restore after the combine.
13196 let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13197 let mut from = if matches!(self.peek(), Token::From) {
13198 self.advance();
13199 Some(self.parse_from_clause()?)
13200 } else {
13201 None
13202 };
13203 // v7.37 D.22 — a set-returning function in the projection with no FROM
13204 // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13205 // rows. Move the first SRF projection item to a FROM-position derived
13206 // table and replace it in the projection with a reference to its output
13207 // column; sibling scalar columns repeat per SRF row. PG names the output
13208 // column after the function (or its AS alias). Reuses the FROM-SRF
13209 // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13210 // works via the targetlist-SRF path.
13211 // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13212 // `SELECT * FROM f(args)` — the record's fields become the columns, which
13213 // is exactly what the function's own row shape already is. Anywhere else
13214 // (per outer row, or beside other items) it would need a real record-typed
13215 // projection, so it says so rather than answering something else.
13216 if let [
13217 SelectItem::Expr {
13218 expr: Expr::FunctionCall { name, args },
13219 ..
13220 },
13221 ] = items.as_slice()
13222 && name == "__record_expand"
13223 {
13224 let Some(Expr::FunctionCall {
13225 name: inner_name,
13226 args: inner_args,
13227 }) = args.first()
13228 else {
13229 return Err(self.err(
13230 "(<expr>).* expands a function's record — it needs a function call".into(),
13231 ));
13232 };
13233 if from.is_some() {
13234 return Err(self.err(
13235 "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13236 .into(),
13237 ));
13238 }
13239 let fn_ref = TableRef {
13240 name: inner_name.clone(),
13241 alias: None,
13242 only: false,
13243 as_of_segment: None,
13244 unnest_expr: None,
13245 unnest_column_aliases: Vec::new(),
13246 with_ordinality: false,
13247 generate_series_args: None,
13248 lateral_subquery: None,
13249 jsonb_each_text_arg: None,
13250 table_fn_call: Some(Box::new((
13251 inner_name.to_ascii_lowercase(),
13252 inner_args.clone(),
13253 ))),
13254 rows_from: None,
13255 json_table: None,
13256 scalar_fn_item: false,
13257 };
13258 items = alloc::vec![SelectItem::Wildcard];
13259 from = Some(FromClause {
13260 primary: fn_ref,
13261 joins: Vec::new(),
13262 });
13263 }
13264 // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13265 // FROM, keeps its marker: the ENGINE lowers it, because naming the
13266 // record's fields takes the catalog. It becomes a LATERAL of the same
13267 // function plus one item per declared column — the machinery rounds 65
13268 // and 69 already built.
13269 // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13270 // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13271 // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13272 // express, since the lifted one becomes a scan and the other would
13273 // expand per its rows (a cross product, not a zip). So when the
13274 // projection holds more than one top-level function call, the lift steps
13275 // aside and the engine's target-list expansion takes the whole list.
13276 let fn_call_items = items
13277 .iter()
13278 .filter(|it| {
13279 matches!(
13280 it,
13281 SelectItem::Expr {
13282 expr: Expr::FunctionCall { .. },
13283 ..
13284 }
13285 )
13286 })
13287 .count();
13288 if from.is_none() && fn_call_items <= 1 {
13289 let mut found: Option<(usize, TableRef, String)> = None;
13290 for (i, item) in items.iter().enumerate() {
13291 if let SelectItem::Expr {
13292 expr: Expr::FunctionCall { name, args },
13293 alias,
13294 } = item
13295 {
13296 let lname = name.to_ascii_lowercase();
13297 let colname = alias.clone().unwrap_or_else(|| lname.clone());
13298 let (unnest, gs) = match lname.as_str() {
13299 "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13300 "generate_series" if (2..=3).contains(&args.len()) => {
13301 (None, Some(args.clone()))
13302 }
13303 // v7.38 (read01) — generate_subscripts(arr, dim) in a
13304 // no-FROM projection yields the 1-based subscripts, i.e.
13305 // generate_series(1, array_length(arr, dim)); an invalid
13306 // dimension makes array_length NULL → 0 rows, as in PG.
13307 "generate_subscripts" if args.len() == 2 => (
13308 None,
13309 Some(alloc::vec![
13310 Expr::Literal(Literal::Integer(1)),
13311 Expr::FunctionCall {
13312 name: "array_length".to_string(),
13313 args: args.clone(),
13314 },
13315 ]),
13316 ),
13317 // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13318 // in a no-FROM projection unnest their *_to_array form.
13319 "string_to_table" | "regexp_split_to_table" => {
13320 let array_fn = if lname == "string_to_table" {
13321 "string_to_array"
13322 } else {
13323 "regexp_split_to_array"
13324 };
13325 (
13326 Some(Box::new(Expr::FunctionCall {
13327 name: array_fn.to_string(),
13328 args: args.clone(),
13329 })),
13330 None,
13331 )
13332 }
13333 // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13334 // a no-FROM projection expand per element. The scalar form
13335 // returns the elements as a TEXT array, so unnest over the
13336 // same call materialises one row each (same rewrite the
13337 // FROM-clause form uses).
13338 "jsonb_array_elements"
13339 | "json_array_elements"
13340 | "jsonb_array_elements_text"
13341 | "json_array_elements_text"
13342 if args.len() == 1 =>
13343 {
13344 (
13345 Some(Box::new(Expr::FunctionCall {
13346 name: lname.clone(),
13347 args: args.clone(),
13348 })),
13349 None,
13350 )
13351 }
13352 // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13353 // in a no-FROM projection expands per match (scalar form
13354 // returns the matches as a TEXT array → unnest).
13355 "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13356 Some(Box::new(Expr::FunctionCall {
13357 name: lname.clone(),
13358 args: args.clone(),
13359 })),
13360 None,
13361 ),
13362 _ => continue,
13363 };
13364 found = Some((
13365 i,
13366 TableRef {
13367 name: colname.clone(),
13368 alias: Some(colname.clone()),
13369 only: false,
13370 as_of_segment: None,
13371 unnest_expr: unnest,
13372 unnest_column_aliases: alloc::vec![colname.clone()],
13373 with_ordinality: false,
13374 generate_series_args: gs,
13375 lateral_subquery: None,
13376 jsonb_each_text_arg: None,
13377 table_fn_call: None,
13378 rows_from: None,
13379 json_table: None,
13380 scalar_fn_item: false,
13381 },
13382 colname,
13383 ));
13384 break;
13385 }
13386 }
13387 if let Some((idx, tref, colname)) = found {
13388 from = Some(FromClause {
13389 primary: tref,
13390 joins: Vec::new(),
13391 });
13392 items[idx] = SelectItem::Expr {
13393 expr: Expr::Column(ColumnName {
13394 qualifier: None,
13395 name: colname.clone(),
13396 }),
13397 alias: Some(colname),
13398 };
13399 }
13400 }
13401 let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13402 let where_ = if matches!(self.peek(), Token::Where) {
13403 self.advance();
13404 Some(self.parse_expr(0)?)
13405 } else {
13406 None
13407 };
13408 let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13409 Some(match acc {
13410 Some(w) => Expr::Binary {
13411 lhs: Box::new(pred),
13412 op: crate::ast::BinOp::And,
13413 rhs: Box::new(w),
13414 },
13415 None => pred,
13416 })
13417 });
13418 self.pending_sample_preds = enclosing_sample_preds;
13419 let mut group_by_all = false;
13420 // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13421 // share one expansion: `grouping_sets` lists the key subsets
13422 // (first = primary, assigned to stmt.group_by; the rest
13423 // become UNION ALL peers), `grouping_universe` is the full
13424 // key list used to compute each peer's dropped keys.
13425 let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13426 let mut grouping_universe: Vec<Expr> = Vec::new();
13427 // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13428 // A BOOL, not the key list: this frame is the statement parser's, and
13429 // round 430 measured that a `Vec` local here is enough on its own to
13430 // tip the 512 KiB nesting guard. The keys are recoverable from
13431 // `grouping_universe`, which a rollup fills with exactly them.
13432 let mut mysql_rollup = false;
13433 let group_by = if matches!(self.peek(), Token::Group) {
13434 self.advance();
13435 if !self.peek_is_by() {
13436 return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13437 }
13438 self.advance();
13439 // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13440 // every non-aggregate SELECT-list item later.
13441 if matches!(self.peek(), Token::All) {
13442 self.advance();
13443 group_by_all = true;
13444 None
13445 } else {
13446 // v7.39 (round 242) — PG's general grouping-element grammar:
13447 // GROUP BY [DISTINCT] element [, element]*, where an element
13448 // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13449 // SETS (…) — mixed freely. Each element yields a list of
13450 // key sets; the query's grouping sets are the CARTESIAN
13451 // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13452 // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13453 // content. ROLLUP/CUBE members may be composite
13454 // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13455 // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13456 // parser handled only a lone ROLLUP/CUBE/GS as the whole
13457 // clause.
13458 let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13459 self.advance();
13460 true
13461 } else {
13462 false
13463 };
13464 let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13465 loop {
13466 element_sets.push(self.parse_grouping_element()?);
13467 if matches!(self.peek(), Token::Comma) {
13468 self.advance();
13469 } else {
13470 break;
13471 }
13472 }
13473 let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13474 for el in &element_sets {
13475 let mut next: Vec<Vec<Expr>> = Vec::new();
13476 for base in &total {
13477 for set in el {
13478 let mut merged = base.clone();
13479 for k in set {
13480 if !merged.iter().any(|m| m == k) {
13481 merged.push(k.clone());
13482 }
13483 }
13484 next.push(merged);
13485 }
13486 }
13487 total = next;
13488 }
13489 // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13490 // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13491 // The keys and the aggregates come out identical; the ROW
13492 // ORDER does not, and that is the part a report depends on.
13493 // MySQL interleaves each group's subtotal right after its
13494 // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13495 // where the union-of-grouping-sets expansion emits every
13496 // leaf first and then every subtotal. MariaDB REFUSES an
13497 // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13498 // order itself — measured on MariaDB 11 and MySQL 9.7, which
13499 // agree on the order and disagree only on whether ORDER BY
13500 // is allowed (MySQL allows it; SPG allows it too, since
13501 // refusing would break the clients that can write it).
13502 if self.mysql_dialect
13503 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13504 && matches!(
13505 self.tokens.get(self.pos + 1),
13506 Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13507 )
13508 {
13509 self.advance(); // WITH
13510 self.advance(); // ROLLUP
13511 let keys = total.into_iter().next().unwrap_or_default();
13512 mysql_rollup = true;
13513 // n+1 prefixes, largest first — the same expansion
13514 // `ROLLUP (…)` produces.
13515 total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13516 }
13517 if distinct_sets {
13518 let mut seen: Vec<Vec<String>> = Vec::new();
13519 total.retain(|set| {
13520 let mut key: Vec<String> =
13521 set.iter().map(|e| alloc::format!("{e}")).collect();
13522 key.sort();
13523 if seen.contains(&key) {
13524 false
13525 } else {
13526 seen.push(key);
13527 true
13528 }
13529 });
13530 }
13531 if total.len() > 1 {
13532 let mut universe: Vec<Expr> = Vec::new();
13533 for set in &total {
13534 for k in set {
13535 if !universe.iter().any(|u| u == k) {
13536 universe.push(k.clone());
13537 }
13538 }
13539 }
13540 grouping_universe = universe;
13541 let primary = total[0].clone();
13542 grouping_sets = total;
13543 Some(primary)
13544 } else {
13545 // One set (a plain GROUP BY list, or a single-set
13546 // spelling like GROUPING SETS ((a, b))). An EMPTY
13547 // single set — GROUPING SETS (()) — stays
13548 // `Some(vec![])`: the grand-total group, which must
13549 // run the aggregate path.
13550 Some(total.into_iter().next().unwrap_or_default())
13551 }
13552 }
13553 } else {
13554 None
13555 };
13556 let having = if matches!(self.peek(), Token::Having) {
13557 self.advance();
13558 Some(self.parse_expr(0)?)
13559 } else {
13560 None
13561 };
13562 // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
13563 // OVER w parsed to a marker above; inline each definition
13564 // into the referencing WindowFunction nodes.
13565 let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
13566 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
13567 self.advance();
13568 loop {
13569 let wname = self.expect_ident_like()?;
13570 if !matches!(self.peek(), Token::As) {
13571 return Err(self.err(format!(
13572 "expected AS after WINDOW {wname}, got {:?}",
13573 self.peek()
13574 )));
13575 }
13576 self.advance();
13577 // v7.39 (round 229) — PG rejects a redefinition outright.
13578 if window_defs
13579 .iter()
13580 .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
13581 {
13582 return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
13583 }
13584 let def = self.parse_over_clause()?;
13585 // A definition may itself copy an earlier one
13586 // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
13587 // so resolve it against the defs already in scope. Same
13588 // copy rules as an `OVER (w1 …)` in the select list.
13589 let mut probe = Expr::WindowFunction {
13590 name: String::new(),
13591 args: Vec::new(),
13592 partition_by: def.0,
13593 order_by: def.1,
13594 frame: def.2,
13595 null_treatment: crate::ast::NullTreatment::Respect,
13596 filter: None,
13597 };
13598 Self::substitute_named_windows(&mut probe, &window_defs)
13599 .map_err(|m| self.err(m))?;
13600 let Expr::WindowFunction {
13601 partition_by,
13602 order_by,
13603 frame,
13604 ..
13605 } = probe
13606 else {
13607 unreachable!("probe is a WindowFunction")
13608 };
13609 window_defs.push((wname, (partition_by, order_by, frame)));
13610 if matches!(self.peek(), Token::Comma) {
13611 self.advance();
13612 continue;
13613 }
13614 break;
13615 }
13616 }
13617 // v7.39 (round 705) — which definitions did anything reference?
13618 // The ones nothing did used to be dropped here, unexamined, so
13619 // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
13620 // definition whether referenced or not. Their key expressions ride
13621 // out on the statement for the engine to resolve.
13622 let mut window_refs: Vec<String> = Vec::new();
13623 if !window_defs.is_empty() {
13624 for it in &items {
13625 if let SelectItem::Expr { expr, .. } = it {
13626 Self::collect_named_window_refs(expr, &mut window_refs);
13627 }
13628 }
13629 }
13630 let window_check_exprs: Vec<Expr> = window_defs
13631 .iter()
13632 .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
13633 .flat_map(|(_, (partition, order, _))| {
13634 partition
13635 .iter()
13636 .cloned()
13637 .chain(order.iter().map(|(e, _, _)| e.clone()))
13638 })
13639 .collect();
13640 if !window_defs.is_empty()
13641 || items
13642 .iter()
13643 .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
13644 {
13645 for it in &mut items {
13646 if let SelectItem::Expr { expr, .. } = it {
13647 Self::substitute_named_windows(expr, &window_defs)
13648 .map_err(|m| self.err(m))?;
13649 }
13650 }
13651 }
13652 // `GROUP BY 1` — positional keys substitute with the Nth
13653 // select item's expression (same contract ORDER BY has had
13654 // since v6.x). Out-of-range positions error.
13655 let group_by = match group_by {
13656 Some(mut keys) => {
13657 for k in &mut keys {
13658 if let Expr::Literal(Literal::Integer(n)) = k {
13659 let idx = *n;
13660 if idx < 1 || idx as usize > items.len() {
13661 return Err(self.err(alloc::format!(
13662 "GROUP BY position {idx} is not in select list"
13663 )));
13664 }
13665 match &items[(idx - 1) as usize] {
13666 SelectItem::Expr { expr, .. } => *k = expr.clone(),
13667 SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
13668 return Err(self.err(alloc::format!(
13669 "GROUP BY position {idx} references a wildcard item"
13670 )));
13671 }
13672 }
13673 }
13674 }
13675 Some(keys)
13676 }
13677 None => None,
13678 };
13679 let mut stmt = SelectStatement {
13680 locking: None,
13681 ctes: Vec::new(),
13682 distinct,
13683 distinct_on,
13684 items,
13685 from,
13686 where_,
13687 group_by,
13688 group_by_all,
13689 having,
13690 unions: Vec::new(),
13691 order_by: Vec::new(),
13692 limit: None,
13693 offset: None,
13694 limit_with_ties: false,
13695 window_check_exprs,
13696 };
13697 // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
13698 // first set is the primary (already on stmt.group_by); each
13699 // further set becomes a UNION ALL peer with its dropped
13700 // keys (universe minus the set) replaced by NULL literals
13701 // in the peer's items and group_by. PG-legal: non-grouped
13702 // select items must be group keys or aggregates, so a
13703 // dropped key's occurrences in the projection are exactly
13704 // the ones to nullify.
13705 // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
13706 // over a plain GROUP BY (every argument must be a group key; the
13707 // mask is then 0) and rejects anything else with 42803. SPG's
13708 // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
13709 // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
13710 // function `grouping`".
13711 if grouping_sets.len() <= 1 {
13712 let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
13713 let mut calls: Vec<Expr> = Vec::new();
13714 for item in &stmt.items {
13715 if let SelectItem::Expr { expr, .. } = item {
13716 Self::collect_grouping_calls(expr, &mut calls);
13717 }
13718 }
13719 if let Some(h) = &stmt.having {
13720 Self::collect_grouping_calls(h, &mut calls);
13721 }
13722 for call in &calls {
13723 let Expr::FunctionCall { args, .. } = call else {
13724 continue;
13725 };
13726 for a in args {
13727 if !keys.iter().any(|k| k == a) {
13728 return Err(self.err(
13729 "arguments to GROUPING must be grouping expressions of the associated query level"
13730 .to_string(),
13731 ));
13732 }
13733 }
13734 }
13735 if !calls.is_empty() {
13736 for item in &mut stmt.items {
13737 if let SelectItem::Expr { expr, .. } = item {
13738 Self::substitute_grouping_calls(expr, &[]);
13739 }
13740 }
13741 if let Some(h) = &mut stmt.having {
13742 Self::substitute_grouping_calls(h, &[]);
13743 }
13744 }
13745 }
13746 if grouping_sets.len() > 1 {
13747 // The primary set's own dropped keys nullify in the
13748 // HEAD's projection too (GROUPING SETS's first set may
13749 // omit keys other sets use).
13750 let primary = grouping_sets[0].clone();
13751 let head_dropped: Vec<Expr> = grouping_universe
13752 .iter()
13753 .filter(|u| !primary.iter().any(|k| k == *u))
13754 .cloned()
13755 .collect();
13756 for set in grouping_sets.iter().skip(1) {
13757 let mut peer = stmt.clone();
13758 peer.unions = Vec::new();
13759 let dropped: Vec<&Expr> = grouping_universe
13760 .iter()
13761 .filter(|u| !set.iter().any(|k| k == *u))
13762 .collect();
13763 // Empty set = grand-total group: `Some(vec![])` forces
13764 // the aggregate path (one collapsed row) instead of a
13765 // per-row passthrough. See the primary-set note above.
13766 peer.group_by = Some(set.clone());
13767 let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
13768 for item in &mut peer.items {
13769 if let SelectItem::Expr { expr, alias } = item {
13770 if dropped.iter().any(|d| *d == expr) {
13771 // v7.39 — keep the dropped key's name on the
13772 // NULL literal so the UNION output column
13773 // (and any top-level ORDER BY on it) still
13774 // resolves.
13775 if alias.is_none()
13776 && let Expr::Column(c) = &expr
13777 {
13778 *alias = Some(c.name.clone());
13779 }
13780 *expr = Expr::Literal(Literal::Null);
13781 } else {
13782 Self::substitute_grouping_calls(expr, &dropped_owned);
13783 }
13784 }
13785 }
13786 if let Some(h) = &mut peer.having {
13787 Self::substitute_grouping_calls(h, &dropped_owned);
13788 }
13789 stmt.unions.push((UnionKind::All, peer));
13790 }
13791 for item in &mut stmt.items {
13792 if let SelectItem::Expr { expr, alias } = item {
13793 if head_dropped.iter().any(|d| d == expr) {
13794 if alias.is_none()
13795 && let Expr::Column(c) = &expr
13796 {
13797 *alias = Some(c.name.clone());
13798 }
13799 *expr = Expr::Literal(Literal::Null);
13800 } else {
13801 Self::substitute_grouping_calls(expr, &head_dropped);
13802 }
13803 }
13804 }
13805 if let Some(h) = &mut stmt.having {
13806 Self::substitute_grouping_calls(h, &head_dropped);
13807 }
13808 // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
13809 // (while `grouping_universe` / the per-branch sets are in scope). For
13810 // each grouping() call in it, inject a per-branch hidden column
13811 // `__grp_ord_K` carrying that branch's mask into the head + every
13812 // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
13813 // preserves this pre-set order_by; the engine strips `__grp_ord_*`
13814 // from the final output. A standalone grouping-set query has ORDER BY
13815 // (not an explicit set-op) next, so consuming it here is safe.
13816 // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
13817 // rollup carries the hierarchical order: sort by the grouping
13818 // keys with the rolled-up NULLs last, which is exactly the
13819 // interleaving both oracles emit. A client's own ORDER BY wins,
13820 // which is what MySQL does (MariaDB refuses to let one be
13821 // written at all).
13822 // The synthesised keys have to travel the SAME path a written
13823 // ORDER BY does: the block below is what turns a `grouping()`
13824 // call into the per-branch `__grp_ord_K` column the engine can
13825 // actually sort on. Bypassing it left a bare `grouping(text)`
13826 // for the evaluator to reject.
13827 let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
13828 self.parse_order_by_keys()?
13829 } else if mysql_rollup {
13830 Self::mysql_rollup_order(&grouping_universe)
13831 } else {
13832 Vec::new()
13833 };
13834 if !synthesised_or_parsed.is_empty() {
13835 let mut order_keys = synthesised_or_parsed;
13836 let mut grp_exprs: Vec<Expr> = Vec::new();
13837 for ob in &order_keys {
13838 Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
13839 }
13840 for (k, gexpr) in grp_exprs.iter().enumerate() {
13841 let colname = alloc::format!("__grp_ord_{k}");
13842 // Head branch (primary set) uses `head_dropped`.
13843 let mut he = gexpr.clone();
13844 Self::substitute_grouping_calls(&mut he, &head_dropped);
13845 stmt.items.push(SelectItem::Expr {
13846 expr: he,
13847 alias: Some(colname.clone()),
13848 });
13849 // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
13850 for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
13851 let set = &grouping_sets[i + 1];
13852 let dropped: Vec<Expr> = grouping_universe
13853 .iter()
13854 .filter(|u| !set.iter().any(|k| k == *u))
13855 .cloned()
13856 .collect();
13857 let mut pe = gexpr.clone();
13858 Self::substitute_grouping_calls(&mut pe, &dropped);
13859 peer.items.push(SelectItem::Expr {
13860 expr: pe,
13861 alias: Some(colname.clone()),
13862 });
13863 }
13864 }
13865 for ob in &mut order_keys {
13866 Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
13867 }
13868 stmt.order_by = order_keys;
13869 }
13870 }
13871 Ok(stmt)
13872 }
13873
13874 /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
13875 /// as ORDER BY keys.
13876 ///
13877 /// Per key: the rollup marker, then the key. Sorting on the key alone
13878 /// is not enough, and a table with a NULL in it says why — MariaDB puts
13879 /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
13880 /// the ROLLUP-introduced NULL last, and both print as NULL.
13881 /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
13882 /// real group including the data-NULL one, 1 only for the row the
13883 /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
13884 /// rolls up to NULL|2, a|1, b|3, NULL|6.
13885 ///
13886 /// `#[inline(never)]`: its locals must not join the statement parser's
13887 /// frame, which round 430 measured sitting against the nesting guard.
13888 #[inline(never)]
13889 fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
13890 let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
13891 for e in keys {
13892 out.push(OrderBy {
13893 expr: Expr::FunctionCall {
13894 name: "grouping".into(),
13895 args: alloc::vec![e.clone()],
13896 },
13897 desc: false,
13898 nulls_first: None,
13899 collation: None,
13900 });
13901 out.push(OrderBy {
13902 expr: e.clone(),
13903 desc: false,
13904 // MySQL orders NULL first on an ascending key.
13905 nulls_first: Some(true),
13906 collation: None,
13907 });
13908 }
13909 out
13910 }
13911
13912 /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
13913 /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
13914 #[inline(never)]
13915 fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
13916 use crate::ast::MaintainKind;
13917 self.skip_paren_option_list();
13918 let kind = match self.peek() {
13919 // `TABLE` and `INDEX` lex as keywords, not identifiers.
13920 Token::Table | Token::Index => {
13921 self.advance();
13922 MaintainKind::ReindexRelation
13923 }
13924 Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
13925 "index" | "table" => {
13926 self.advance();
13927 MaintainKind::ReindexRelation
13928 }
13929 "schema" => {
13930 self.advance();
13931 MaintainKind::ReindexSchema
13932 }
13933 "system" | "database" => {
13934 self.advance();
13935 MaintainKind::Whole
13936 }
13937 // PG requires the object type; anything else is the
13938 // caller's problem, not something to swallow.
13939 _ => MaintainKind::ReindexRelation,
13940 },
13941 _ => MaintainKind::Whole,
13942 };
13943 // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
13944 // allows the plain form, so the modifier is recorded rather than
13945 // skipped. It still has no effect on how the reindex runs.
13946 let mut concurrently = false;
13947 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
13948 self.advance();
13949 concurrently = true;
13950 }
13951 let target = self.take_optional_maintain_name();
13952 self.consume_until_statement_boundary();
13953 Ok(Statement::Maintain {
13954 kind,
13955 concurrently,
13956 target,
13957 })
13958 }
13959
13960 /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
13961 /// and `CLUSTER [VERBOSE] <index> ON <table>`.
13962 #[inline(never)]
13963 fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
13964 use crate::ast::MaintainKind;
13965 self.skip_paren_option_list();
13966 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
13967 self.advance();
13968 }
13969 let target = self.take_optional_maintain_name();
13970 self.consume_until_statement_boundary();
13971 Ok(Statement::Maintain {
13972 kind: if target.is_some() {
13973 MaintainKind::ClusterRelation
13974 } else {
13975 MaintainKind::Whole
13976 },
13977 // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
13978 // transaction block quite happily (measured).
13979 concurrently: false,
13980 target,
13981 })
13982 }
13983
13984 /// The next token as a relation / schema name, when there is one.
13985 fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
13986 match self.peek() {
13987 Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
13988 Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
13989 _ => None,
13990 },
13991 _ => None,
13992 }
13993 }
13994
13995 /// A parenthesised option list, absorbed.
13996 fn skip_paren_option_list(&mut self) {
13997 if !matches!(self.peek(), Token::LParen) {
13998 return;
13999 }
14000 let mut depth = 0usize;
14001 loop {
14002 match self.advance() {
14003 Token::LParen => depth += 1,
14004 Token::RParen => {
14005 depth -= 1;
14006 if depth == 0 {
14007 return;
14008 }
14009 }
14010 Token::Eof => return,
14011 _ => {}
14012 }
14013 }
14014 }
14015
14016 /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14017 /// column list.
14018 ///
14019 /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14020 /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14021 /// / ALL. The three that describe physical storage have no meaning
14022 /// here, so they parse and change nothing rather than making a
14023 /// dump that mentions them fail to load.
14024 ///
14025 /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14026 /// parse chain the nesting sentinel is tuned against.
14027 #[inline(never)]
14028 fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14029 self.advance(); // LIKE
14030 let source = self.expect_ident_like()?;
14031 let mut options = crate::ast::LikeOptions::default();
14032 loop {
14033 let including = match self.peek() {
14034 Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14035 Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14036 _ => break,
14037 };
14038 self.advance();
14039 // `ALL` lexes as its own keyword, not an identifier.
14040 let opt = if matches!(self.peek(), Token::All) {
14041 self.advance();
14042 alloc::string::String::from("all")
14043 } else {
14044 self.expect_ident_like()?
14045 };
14046 let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14047 o.defaults = on;
14048 o.constraints = on;
14049 o.identity = on;
14050 o.generated = on;
14051 o.indexes = on;
14052 o.comments = on;
14053 };
14054 match opt.to_ascii_lowercase().as_str() {
14055 "all" => set(&mut options, including),
14056 "defaults" => options.defaults = including,
14057 "constraints" => options.constraints = including,
14058 "identity" => options.identity = including,
14059 "generated" => options.generated = including,
14060 "indexes" => options.indexes = including,
14061 "comments" => options.comments = including,
14062 // No storage model to copy into.
14063 "storage" | "statistics" | "compression" => {}
14064 other => {
14065 return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14066 }
14067 }
14068 }
14069 Ok(crate::ast::LikeSpec {
14070 source,
14071 at,
14072 options,
14073 })
14074 }
14075
14076 fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14077 // Caller already consumed CREATE; we're sitting on TABLE.
14078 debug_assert!(matches!(self.peek(), Token::Table));
14079 self.advance();
14080 let if_not_exists = self.consume_if_not_exists();
14081 let name = self.expect_ident_like()?;
14082 // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14083 // child shape has no column list; the child inherits its
14084 // columns from the parent at engine-DDL time. Detect it
14085 // before the `(` requirement below.
14086 if matches!(self.peek(), Token::Partition)
14087 && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14088 {
14089 self.advance(); // PARTITION
14090 self.advance(); // of
14091 let partition_of = self.parse_partition_of_tail()?;
14092 return Ok(Statement::CreateTable(CreateTableStatement {
14093 temporary: false,
14094 name,
14095 columns: Vec::new(),
14096 like_specs: Vec::new(),
14097 inherits: Vec::new(),
14098 if_not_exists,
14099 foreign_keys: Vec::new(),
14100 table_constraints: Vec::new(),
14101 partition_by: None,
14102 partition_of: Some(partition_of),
14103 }));
14104 }
14105 // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14106 // the materialized-view materialisation path (run the SELECT, infer the
14107 // column types, create + populate the table) but marks the node so the
14108 // executor creates a plain table without a mat-view registry entry.
14109 if matches!(self.peek(), Token::As) {
14110 self.advance();
14111 let body_stmt = self.parse_select_stmt()?;
14112 let Statement::Select(body) = body_stmt else {
14113 return Err(self.err(format!(
14114 "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14115 )));
14116 };
14117 let with_data = self.parse_optional_with_data(true)?;
14118 return Ok(Statement::CreateMaterializedView(
14119 crate::ast::CreateMaterializedViewStatement {
14120 temporary: false,
14121 name,
14122 if_not_exists,
14123 columns: Vec::new(),
14124 body,
14125 with_data,
14126 as_plain_table: true,
14127 },
14128 ));
14129 }
14130 if !matches!(self.peek(), Token::LParen) {
14131 return Err(self.err(format!(
14132 "expected '(' after table name, got {:?}",
14133 self.peek()
14134 )));
14135 }
14136 self.advance();
14137 let mut columns = Vec::new();
14138 let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14139 let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14140 let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14141 loop {
14142 // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14143 // column list. It is how a child that adds nothing of its own is
14144 // written, and this loop demanded at least one entry: `syntax
14145 // error at or near ")"`. The child takes the parent's columns,
14146 // which the INHERITS clause already arranges.
14147 if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14148 self.advance();
14149 break;
14150 }
14151 // v7.6.0 / v7.9.18 — distinguish table-level constraint
14152 // clauses from column definitions. Constraints start
14153 // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14154 // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14155 // a column.
14156 if self.peek_table_level_pk_start() {
14157 table_constraints.push(self.parse_table_level_primary_key()?);
14158 } else if matches!(self.peek(), Token::Like) {
14159 // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14160 // <opt> ]*`. The source table's shape lives in the catalog,
14161 // so this records the clause and the engine expands it.
14162 like_specs.push(self.parse_create_table_like(columns.len())?);
14163 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14164 // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14165 table_constraints.push(self.parse_table_level_exclude()?);
14166 } else if self.peek_table_level_unique_start() {
14167 table_constraints.push(self.parse_table_level_unique()?);
14168 } else if self.peek_table_level_check_start() {
14169 // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14170 table_constraints.push(self.parse_table_level_check()?);
14171 } else if self.peek_mysql_inline_key_start() {
14172 // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14173 // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14174 // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14175 // inside the column list. Skip name + paren list;
14176 // for UNIQUE KEY, register as a UC.
14177 if let Some(uc) = self.parse_mysql_inline_key()? {
14178 table_constraints.push(uc);
14179 }
14180 } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14181 // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14182 // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14183 // CHECK is named, and the named-CONSTRAINT arm used
14184 // to accept FOREIGN KEY only. The name is accepted
14185 // and discarded — same handling as every other SPG
14186 // constraint name.
14187 self.advance(); // CONSTRAINT
14188 // v7.39 (read01 round 48) — the name is kept now: the schema
14189 // stores it, so DROP / RENAME CONSTRAINT can find it.
14190 let con_name = self.expect_ident_like()?;
14191 let mut tc = match kind {
14192 NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14193 NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14194 NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14195 NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14196 };
14197 match &mut tc {
14198 crate::ast::TableConstraint::Check { name, .. }
14199 | crate::ast::TableConstraint::Unique { name, .. }
14200 | crate::ast::TableConstraint::PrimaryKey { name, .. }
14201 | crate::ast::TableConstraint::Exclude { name, .. } => {
14202 *name = Some(con_name);
14203 }
14204 _ => {}
14205 }
14206 table_constraints.push(tc);
14207 } else if self.peek_constraint_or_fk_start() {
14208 foreign_keys.push(self.parse_table_level_fk()?);
14209 } else {
14210 let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14211 // v7.13.0 — fold inline UNIQUE / CHECK column
14212 // constraints into table-level entries so the
14213 // engine path stays uniform.
14214 if col.is_unique {
14215 table_constraints.push(crate::ast::TableConstraint::Unique {
14216 name: None,
14217 columns: alloc::vec![col.name.clone()],
14218 nulls_not_distinct: col.unique_nulls_not_distinct,
14219 deferrable: col.constraint_deferrable,
14220 initially_deferred: col.constraint_initially_deferred,
14221 });
14222 }
14223 if let Some(check_expr) = col.check.clone() {
14224 table_constraints.push(crate::ast::TableConstraint::Check {
14225 name: None,
14226 expr: check_expr,
14227 not_valid: false,
14228 });
14229 }
14230 columns.push(col);
14231 if let Some(fk) = col_level_fk {
14232 foreign_keys.push(fk);
14233 }
14234 }
14235 match self.peek() {
14236 Token::Comma => {
14237 self.advance();
14238 }
14239 Token::RParen => {
14240 self.advance();
14241 break;
14242 }
14243 other => {
14244 return Err(
14245 self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14246 );
14247 }
14248 }
14249 }
14250 // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14251 // `CREATE TABLE k (LIKE t)` is a complete definition even though
14252 // nothing is written between the parentheses.
14253 // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14254 // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14255 // empty parentheses were a parse error in their own right — quite apart
14256 // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14257 // SPG does not have (filed separately).
14258 let _ = &like_specs;
14259 // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14260 // It sits between the column list and the MySQL table options,
14261 // and it was a syntax error until this round.
14262 let mut inherits: Vec<String> = Vec::new();
14263 if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14264 if k.eq_ignore_ascii_case("inherits"))
14265 {
14266 self.advance();
14267 if !matches!(self.peek(), Token::LParen) {
14268 return Err(self.err(alloc::format!(
14269 "expected ( after INHERITS, got {:?}",
14270 self.peek()
14271 )));
14272 }
14273 self.advance();
14274 loop {
14275 inherits.push(self.expect_ident_like()?);
14276 if matches!(self.peek(), Token::Comma) {
14277 self.advance();
14278 continue;
14279 }
14280 break;
14281 }
14282 if !matches!(self.peek(), Token::RParen) {
14283 return Err(self.err(alloc::format!(
14284 "expected ) closing INHERITS, got {:?}",
14285 self.peek()
14286 )));
14287 }
14288 self.advance();
14289 }
14290 // v7.14.0 — consume MySQL/MariaDB table options after the
14291 // closing `)`. mysqldump emits things like
14292 // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14293 // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14294 // SPG accepts all forms as no-ops (each option is
14295 // `<ident> [=] <ident-or-string>` separated by whitespace).
14296 self.consume_mysql_table_options();
14297 // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14298 // SPG has no per-table reloptions, so accept and ignore them so a
14299 // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14300 self.consume_with_reloptions();
14301 // v7.37.6-B — declarative-partition-parent suffix
14302 // (`PARTITION BY RANGE (key_col)`) sits after the column
14303 // list + MySQL table-options. v7.37.6-B only accepts RANGE
14304 // and locks the key column at one ident; the engine then
14305 // verifies the column type is TIMESTAMPTZ.
14306 let partition_by = if matches!(self.peek(), Token::Partition) {
14307 self.advance(); // PARTITION
14308 if !self.peek_is_by() {
14309 return Err(self.err(format!(
14310 "expected BY after PARTITION, got {:?}",
14311 self.peek()
14312 )));
14313 }
14314 self.advance();
14315 Some(self.parse_partition_by_tail()?)
14316 } else {
14317 None
14318 };
14319 Ok(Statement::CreateTable(CreateTableStatement {
14320 temporary: false,
14321 name,
14322 columns,
14323 like_specs,
14324 inherits,
14325 if_not_exists,
14326 foreign_keys,
14327 table_constraints,
14328 partition_by,
14329 partition_of: None,
14330 }))
14331 }
14332
14333 /// v7.37.6-B — case-insensitive ident match helper for the
14334 /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14335 /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14336 /// didn't burn a global keyword slot for each (see the
14337 /// `Token::Partition` doc-comment in `lexer.rs`).
14338 fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14339 matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14340 }
14341
14342 /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14343 /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14344 fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14345 use crate::ast::{PartitionBySpec, PartitionKindAst};
14346 let kind = match self.peek() {
14347 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14348 self.advance();
14349 PartitionKindAst::Range
14350 }
14351 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14352 self.advance();
14353 PartitionKindAst::List
14354 }
14355 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14356 self.advance();
14357 PartitionKindAst::Hash
14358 }
14359 other => {
14360 return Err(self.err(format!(
14361 "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14362 )));
14363 }
14364 };
14365 if !matches!(self.peek(), Token::LParen) {
14366 return Err(self.err(format!(
14367 "expected '(' after PARTITION BY <strategy>, got {:?}",
14368 self.peek()
14369 )));
14370 }
14371 self.advance();
14372 let mut key_columns = Vec::new();
14373 loop {
14374 key_columns.push(self.expect_ident_like()?);
14375 match self.peek() {
14376 Token::Comma => {
14377 self.advance();
14378 }
14379 Token::RParen => {
14380 self.advance();
14381 break;
14382 }
14383 other => {
14384 return Err(self.err(format!(
14385 "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14386 )));
14387 }
14388 }
14389 }
14390 if key_columns.is_empty() {
14391 return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14392 }
14393 Ok(PartitionBySpec { kind, key_columns })
14394 }
14395
14396 /// v7.37.6-B — after `PARTITION OF`, expect
14397 /// <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14398 /// or
14399 /// <parent> DEFAULT
14400 fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14401 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14402 let parent_name = self.expect_ident_like()?;
14403 // v7.37.6-B rejects an explicit column list — the child
14404 // inherits from the parent. mailrs round-7 taught us that
14405 // CREATE TABLE-side schema reconciliation hides drift, so
14406 // we surface this as a parse error rather than silently
14407 // ignoring user columns.
14408 if matches!(self.peek(), Token::LParen) {
14409 return Err(self.err(
14410 "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14411 at v7.37.6-B; the child inherits its columns from the parent"
14412 .to_string(),
14413 ));
14414 }
14415 let bounds = match self.peek() {
14416 Token::Default => {
14417 self.advance();
14418 PartitionOfBoundsAst::Default
14419 }
14420 Token::For => {
14421 self.advance();
14422 if !matches!(self.peek(), Token::Values) {
14423 return Err(
14424 self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14425 );
14426 }
14427 self.advance();
14428 // WITH is not a reserved Token in the lexer — it lexes
14429 // as Token::Ident("with"). Disambiguate manually.
14430 let want_with = matches!(
14431 self.peek(),
14432 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14433 );
14434 if want_with {
14435 self.advance();
14436 if !matches!(self.peek(), Token::LParen) {
14437 return Err(self.err(format!(
14438 "expected '(' after FOR VALUES WITH, got {:?}",
14439 self.peek()
14440 )));
14441 }
14442 self.advance();
14443 let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14444 loop {
14445 let key = self.expect_ident_like()?;
14446 let n = match self.peek().clone() {
14447 Token::Integer(v) if u32::try_from(v).is_ok() => {
14448 self.advance();
14449 v as u32
14450 }
14451 other => {
14452 return Err(self.err(format!(
14453 "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14454 )));
14455 }
14456 };
14457 match key.to_ascii_uppercase().as_str() {
14458 "MODULUS" => modulus = Some(n),
14459 "REMAINDER" => remainder = Some(n),
14460 other => {
14461 return Err(self.err(format!(
14462 "FOR VALUES WITH: unknown key {other:?}; \
14463 expected MODULUS or REMAINDER"
14464 )));
14465 }
14466 }
14467 match self.peek() {
14468 Token::Comma => {
14469 self.advance();
14470 }
14471 Token::RParen => {
14472 self.advance();
14473 break;
14474 }
14475 other => {
14476 return Err(self.err(format!(
14477 "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14478 )));
14479 }
14480 }
14481 }
14482 let modulus = modulus
14483 .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14484 let remainder = remainder.ok_or_else(|| {
14485 self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14486 })?;
14487 if modulus == 0 {
14488 return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14489 }
14490 if remainder >= modulus {
14491 return Err(self.err(format!(
14492 "FOR VALUES WITH: REMAINDER ({remainder}) \
14493 must be < MODULUS ({modulus})"
14494 )));
14495 }
14496 PartitionOfBoundsAst::Hash { modulus, remainder }
14497 } else {
14498 match self.peek() {
14499 Token::From => {
14500 self.advance();
14501 let lower = Box::new(self.parse_partition_bound_expr()?);
14502 if !matches!(self.peek(), Token::To) {
14503 return Err(self.err(format!(
14504 "expected TO after FROM (...), got {:?}",
14505 self.peek()
14506 )));
14507 }
14508 self.advance();
14509 let upper = Box::new(self.parse_partition_bound_expr()?);
14510 PartitionOfBoundsAst::Range { lower, upper }
14511 }
14512 // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14513 Token::In => {
14514 self.advance();
14515 if !matches!(self.peek(), Token::LParen) {
14516 return Err(self.err(format!(
14517 "expected '(' after FOR VALUES IN, got {:?}",
14518 self.peek()
14519 )));
14520 }
14521 self.advance();
14522 let mut values = Vec::new();
14523 loop {
14524 values.push(self.parse_expr(0)?);
14525 match self.peek() {
14526 Token::Comma => {
14527 self.advance();
14528 }
14529 Token::RParen => {
14530 self.advance();
14531 break;
14532 }
14533 other => {
14534 return Err(self.err(format!(
14535 "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
14536 )));
14537 }
14538 }
14539 }
14540 if values.is_empty() {
14541 return Err(self.err(
14542 "FOR VALUES IN requires at least one literal".to_string(),
14543 ));
14544 }
14545 PartitionOfBoundsAst::List { values }
14546 }
14547 other => {
14548 return Err(self.err(format!(
14549 "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
14550 )));
14551 }
14552 }
14553 }
14554 }
14555 other => {
14556 return Err(self.err(format!(
14557 "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
14558 )));
14559 }
14560 };
14561 Ok(PartitionOfSpec {
14562 parent_name,
14563 bounds,
14564 })
14565 }
14566
14567 /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
14568 /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
14569 /// markers (no-arg builtins) so the engine resolves them
14570 /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
14571 fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
14572 if !matches!(self.peek(), Token::LParen) {
14573 return Err(self.err(format!(
14574 "expected '(' before partition bound, got {:?}",
14575 self.peek()
14576 )));
14577 }
14578 self.advance();
14579 let expr = match self.peek() {
14580 Token::Ident(s) | Token::QuotedIdent(s)
14581 if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
14582 {
14583 let name = s.to_ascii_uppercase();
14584 self.advance();
14585 crate::ast::Expr::FunctionCall {
14586 name,
14587 args: Vec::new(),
14588 }
14589 }
14590 _ => self.parse_expr(0)?,
14591 };
14592 if !matches!(self.peek(), Token::RParen) {
14593 return Err(self.err(format!(
14594 "expected ')' after partition bound, got {:?}",
14595 self.peek()
14596 )));
14597 }
14598 self.advance();
14599 Ok(expr)
14600 }
14601
14602 /// v7.14.0 — true when the next tokens look like an inline
14603 /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
14604 /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
14605 /// — each followed by an optional name + `(...)`. Critical:
14606 /// a column NAMED `key` / `index` (PG accepts as ident) must
14607 /// NOT be mistaken for the KEY constraint shape. We disambig
14608 /// by requiring the keyword to be followed by either `(` or
14609 /// `<ident> (`.
14610 fn peek_mysql_inline_key_start(&self) -> bool {
14611 let cur = self.peek();
14612 // Shapes:
14613 // KEY (cols)
14614 // KEY name (cols)
14615 // INDEX (cols)
14616 // INDEX name (cols)
14617 // UNIQUE KEY [name] (cols)
14618 // UNIQUE INDEX [name] (cols)
14619 // FULLTEXT [KEY|INDEX] [name] (cols)
14620 // SPATIAL [KEY|INDEX] [name] (cols)
14621 let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
14622 // tokens at skip = the position AFTER the index-form
14623 // keywords (KEY/INDEX) have been consumed.
14624 match self.tokens.get(skip) {
14625 Some(Token::LParen) => true,
14626 Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
14627 matches!(self.tokens.get(skip + 1), Some(Token::LParen))
14628 }
14629 _ => false,
14630 }
14631 };
14632 // `INDEX` lexes as Token::Index (reserved), not as
14633 // Token::Ident("index"). Both shapes count as a KEY/INDEX
14634 // start; the peek helper below handles either.
14635 let is_key_or_index_tok = |t: &Token| -> bool {
14636 matches!(t, Token::Index)
14637 || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
14638 };
14639 match cur {
14640 Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
14641 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14642 after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
14643 }
14644 Token::Ident(s)
14645 if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
14646 {
14647 let nxt = self.tokens.get(self.pos + 1);
14648 let after_after = if nxt.is_some_and(is_key_or_index_tok) {
14649 self.pos + 2
14650 } else {
14651 self.pos + 1
14652 };
14653 after_keyword_followed_by_paren_or_ident_paren(after_after)
14654 }
14655 Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
14656 let nxt = self.tokens.get(self.pos + 1);
14657 if !nxt.is_some_and(is_key_or_index_tok) {
14658 return false;
14659 }
14660 after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
14661 }
14662 _ => false,
14663 }
14664 }
14665
14666 /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
14667 /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
14668 /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
14669 /// returns Some(TableConstraint::Index) so the engine builds
14670 /// a real BTree index on the leading column (mysqldump
14671 /// `KEY idx_posts_author (author_id)` shape).
14672 /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
14673 /// (the storage layer has no matching AM).
14674 fn parse_mysql_inline_key(
14675 &mut self,
14676 ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
14677 // Detect UNIQUE prefix.
14678 let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
14679 {
14680 self.advance();
14681 true
14682 } else {
14683 false
14684 };
14685 // Consume FULLTEXT / SPATIAL prefix and record which one
14686 // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
14687 // dedicated TableConstraint variant so the engine can
14688 // build a tsvector-GIN; SPATIAL still has no matching
14689 // AM, so it falls back to accept-as-no-op.
14690 let mut is_fulltext = false;
14691 let mut is_spatial = false;
14692 if let Token::Ident(s) = self.peek().clone() {
14693 if s.eq_ignore_ascii_case("fulltext") {
14694 self.advance();
14695 is_fulltext = true;
14696 } else if s.eq_ignore_ascii_case("spatial") {
14697 self.advance();
14698 is_spatial = true;
14699 }
14700 }
14701 // KEY / INDEX keyword. `INDEX` lexes as Token::Index
14702 // (reserved); accept either token shape.
14703 match self.peek() {
14704 Token::Index => {
14705 self.advance();
14706 }
14707 Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
14708 self.advance();
14709 }
14710 other => {
14711 return Err(self.err(alloc::format!(
14712 "expected KEY/INDEX in inline index declaration, got {other:?}"
14713 )));
14714 }
14715 }
14716 // Optional index name (an ident before the `(`).
14717 // v7.15.0 — capture the name when present so the engine
14718 // builds the secondary index under the user's chosen
14719 // name (matches mysqldump's `KEY idx_x (col)` shape).
14720 let mut idx_name: Option<String> = None;
14721 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
14722 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
14723 {
14724 if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
14725 idx_name = Some(s);
14726 }
14727 }
14728 // Optional `USING BTREE` / `USING HASH` (MySQL).
14729 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
14730 self.advance();
14731 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14732 self.advance();
14733 }
14734 }
14735 // Required column list `(col [, col]*)`.
14736 if !matches!(self.peek(), Token::LParen) {
14737 return Err(self.err(alloc::format!(
14738 "expected '(' in inline KEY/INDEX, got {:?}",
14739 self.peek()
14740 )));
14741 }
14742 self.advance();
14743 let mut cols: Vec<String> = Vec::new();
14744 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
14745 self.advance();
14746 cols.push(s);
14747 // Skip optional `(length)` per-column prefix.
14748 if matches!(self.peek(), Token::LParen) {
14749 let mut depth = 1usize;
14750 self.advance();
14751 while depth > 0 {
14752 match self.peek() {
14753 Token::LParen => depth += 1,
14754 Token::RParen => depth -= 1,
14755 Token::Eof => break,
14756 _ => {}
14757 }
14758 self.advance();
14759 }
14760 }
14761 // Skip optional ASC / DESC.
14762 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
14763 || matches!(self.peek(), Token::Asc | Token::Desc)
14764 {
14765 self.advance();
14766 }
14767 if matches!(self.peek(), Token::Comma) {
14768 self.advance();
14769 continue;
14770 }
14771 break;
14772 }
14773 if matches!(self.peek(), Token::RParen) {
14774 self.advance();
14775 }
14776 // Trailing options on the inline index — comment / etc.
14777 // Skip until comma or `)`.
14778 while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
14779 self.advance();
14780 }
14781 if cols.is_empty() {
14782 return Ok(None);
14783 }
14784 if is_unique {
14785 // Carry the captured idx_name on UNIQUE too so future
14786 // engine work can name the underlying BTree
14787 // accordingly; today the unique-constraint installer
14788 // synthesises the name itself, but Display round-trip
14789 // benefits from preserving it.
14790 Ok(Some(crate::ast::TableConstraint::Unique {
14791 name: idx_name,
14792 columns: cols,
14793 nulls_not_distinct: false,
14794 // MySQL inline UNIQUE KEY has no deferral vocabulary.
14795 deferrable: false,
14796 initially_deferred: false,
14797 }))
14798 } else if is_fulltext {
14799 // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
14800 // routes through `TableConstraint::FulltextIndex`;
14801 // the engine builds a tsvector-GIN over each named
14802 // column so MATCH AGAINST gets a real inverted
14803 // index instead of a silently-dropped declaration.
14804 Ok(Some(crate::ast::TableConstraint::FulltextIndex {
14805 name: idx_name,
14806 columns: cols,
14807 }))
14808 } else if is_spatial {
14809 // SPG has no native SPATIAL AM. Accept-as-no-op
14810 // (declaration is parsed, but no index is built).
14811 Ok(None)
14812 } else {
14813 // v7.15.0 — plain KEY / INDEX builds a real BTree
14814 // secondary index.
14815 Ok(Some(crate::ast::TableConstraint::Index {
14816 name: idx_name,
14817 columns: cols,
14818 }))
14819 }
14820 }
14821
14822 /// v7.14.0 — consume MySQL/MariaDB table-options tail after
14823 /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
14824 /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
14825 /// (in any order, separated by whitespace).
14826 /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
14827 /// storage-parameter clause on CREATE TABLE. SPG has no per-table
14828 /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
14829 /// bare ident here, and only the parenthesised form is reloptions (so this
14830 /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
14831 fn consume_with_reloptions(&mut self) {
14832 let is_with = matches!(
14833 self.peek(),
14834 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14835 );
14836 if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
14837 return;
14838 }
14839 self.advance(); // WITH
14840 self.advance(); // (
14841 let mut depth = 1u32;
14842 while depth > 0 && !matches!(self.peek(), Token::Eof) {
14843 match self.peek() {
14844 Token::LParen => depth += 1,
14845 Token::RParen => depth -= 1,
14846 _ => {}
14847 }
14848 self.advance();
14849 }
14850 }
14851
14852 fn consume_mysql_table_options(&mut self) {
14853 loop {
14854 // Heuristic: a table option is an ident (or `DEFAULT`
14855 // reserved keyword) followed by `=` and an
14856 // ident / string / integer.
14857 let name_lc = match self.peek().clone() {
14858 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
14859 Token::Default => alloc::string::String::from("default"),
14860 _ => break,
14861 };
14862 let known = matches!(
14863 name_lc.as_str(),
14864 "engine"
14865 | "default"
14866 | "charset"
14867 | "collate"
14868 | "auto_increment"
14869 | "row_format"
14870 | "comment"
14871 | "pack_keys"
14872 | "stats_persistent"
14873 | "stats_auto_recalc"
14874 | "stats_sample_pages"
14875 | "key_block_size"
14876 | "tablespace"
14877 | "min_rows"
14878 | "max_rows"
14879 | "checksum"
14880 | "delay_key_write"
14881 | "insert_method"
14882 | "data"
14883 | "index"
14884 | "encryption"
14885 | "compression"
14886 );
14887 if !known {
14888 break;
14889 }
14890 self.advance(); // option name
14891 // `DEFAULT` optional prefix is followed by `CHARSET` /
14892 // `COLLATE`; consume the next ident too.
14893 if name_lc == "default" {
14894 if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
14895 self.advance();
14896 }
14897 }
14898 if matches!(self.peek(), Token::Eq) {
14899 self.advance();
14900 }
14901 match self.peek() {
14902 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_) | Token::Integer(_) => {
14903 self.advance();
14904 }
14905 _ => {}
14906 }
14907 }
14908 }
14909
14910 /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
14911 /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
14912 /// sure (otherwise a column literally named `primary` would
14913 /// be mistaken).
14914 fn peek_table_level_pk_start(&self) -> bool {
14915 let cur = self.peek();
14916 let nxt = self.tokens.get(self.pos + 1);
14917 let nxt2 = self.tokens.get(self.pos + 2);
14918 let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
14919 let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
14920 let is_lparen = matches!(nxt2, Some(Token::LParen));
14921 is_primary && is_key && is_lparen
14922 }
14923
14924 /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
14925 /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
14926 /// (mailrs round-5 G10).
14927 fn peek_table_level_unique_start(&self) -> bool {
14928 let cur = self.peek();
14929 let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
14930 if !is_unique {
14931 return false;
14932 }
14933 let n1 = self.tokens.get(self.pos + 1);
14934 // Plain `UNIQUE (…)`.
14935 if matches!(n1, Some(Token::LParen)) {
14936 return true;
14937 }
14938 // `UNIQUE NULLS [NOT] DISTINCT (…)`.
14939 let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
14940 if !is_nulls {
14941 return false;
14942 }
14943 let n2 = self.tokens.get(self.pos + 2);
14944 let n3 = self.tokens.get(self.pos + 3);
14945 let n4 = self.tokens.get(self.pos + 4);
14946 // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
14947 if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
14948 return true;
14949 }
14950 // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
14951 if matches!(n2, Some(Token::Not))
14952 && matches!(n3, Some(Token::Distinct))
14953 && matches!(n4, Some(Token::LParen))
14954 {
14955 return true;
14956 }
14957 false
14958 }
14959
14960 fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14961 self.advance(); // PRIMARY
14962 self.advance(); // KEY
14963 let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
14964 // v7.39 (round 711) — the trailer's values are CARRIED now; round
14965 // 621 consumed and dropped them (the storing half of F08).
14966 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14967 Ok(crate::ast::TableConstraint::PrimaryKey {
14968 name: None,
14969 columns,
14970 deferrable,
14971 initially_deferred,
14972 })
14973 }
14974
14975 fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
14976 self.advance(); // UNIQUE
14977 // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
14978 // (mailrs round-5 G10, PG 15+ surface). Default behaviour
14979 // is `NULLS DISTINCT` per the SQL standard.
14980 let mut nulls_not_distinct = false;
14981 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
14982 let n1 = self.tokens.get(self.pos + 1);
14983 let n2 = self.tokens.get(self.pos + 2);
14984 let is_not = matches!(n1, Some(Token::Not));
14985 let is_distinct = matches!(n2, Some(Token::Distinct));
14986 if is_not && is_distinct {
14987 self.advance(); // NULLS
14988 self.advance(); // NOT
14989 self.advance(); // DISTINCT
14990 nulls_not_distinct = true;
14991 } else if matches!(n1, Some(Token::Distinct)) {
14992 self.advance(); // NULLS
14993 self.advance(); // DISTINCT
14994 }
14995 }
14996 let columns = self.parse_paren_ident_list("UNIQUE")?;
14997 let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
14998 Ok(crate::ast::TableConstraint::Unique {
14999 name: None,
15000 columns,
15001 nulls_not_distinct,
15002 deferrable,
15003 initially_deferred,
15004 })
15005 }
15006
15007 /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15008 /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15009 /// expression.
15010 /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15011 /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15012 /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15013 /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15014 /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15015 /// commit: `NOT` starts no other suffix here, but reading both
15016 /// tokens before advancing keeps the caller's error message intact
15017 /// if someone writes `NOT NULL` by mistake.
15018 fn parse_not_valid_suffix(&mut self) -> bool {
15019 if !matches!(self.peek(), Token::Not) {
15020 return false;
15021 }
15022 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15023 {
15024 return false;
15025 }
15026 self.advance();
15027 self.advance();
15028 true
15029 }
15030
15031 fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15032 self.advance(); // EXCLUDE
15033 // Optional `USING <method>`.
15034 let mut method = None;
15035 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15036 self.advance();
15037 method = Some(match self.advance() {
15038 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15039 other => {
15040 return Err(self.err(alloc::format!(
15041 "expected index method after USING, got {other:?}"
15042 )));
15043 }
15044 });
15045 }
15046 if !matches!(self.peek(), Token::LParen) {
15047 return Err(self.err(alloc::format!(
15048 "expected '(' after EXCLUDE, got {:?}",
15049 self.peek()
15050 )));
15051 }
15052 self.advance();
15053 let mut elements: Vec<(String, String)> = Vec::new();
15054 loop {
15055 let col = match self.advance() {
15056 Token::Ident(s) | Token::QuotedIdent(s) => s,
15057 other => {
15058 return Err(self.err(alloc::format!(
15059 "expected column name in EXCLUDE, got {other:?}"
15060 )));
15061 }
15062 };
15063 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15064 return Err(self.err(alloc::format!(
15065 "expected WITH after EXCLUDE column, got {:?}",
15066 self.peek()
15067 )));
15068 }
15069 self.advance();
15070 let op = match self.advance() {
15071 Token::InetOverlap => String::from("&&"),
15072 Token::Intersects => String::from("?#"),
15073 Token::IsBelow => String::from("<^"),
15074 Token::IsAbove => String::from(">^"),
15075 Token::PatternLt => String::from("~<~"),
15076 Token::PatternLtEq => String::from("~<=~"),
15077 Token::PatternGt => String::from("~>~"),
15078 Token::PatternGtEq => String::from("~>=~"),
15079 Token::TsMatchOld => String::from("@@@"),
15080 Token::Eq => String::from("="),
15081 Token::JsonContains => String::from("@>"),
15082 Token::JsonContainedBy => String::from("<@"),
15083 Token::OverLeft => String::from("&<"),
15084 Token::OverRight => String::from("&>"),
15085 other => {
15086 return Err(self.err(alloc::format!(
15087 "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15088 )));
15089 }
15090 };
15091 elements.push((col, op));
15092 if matches!(self.peek(), Token::Comma) {
15093 self.advance();
15094 continue;
15095 }
15096 break;
15097 }
15098 if !matches!(self.peek(), Token::RParen) {
15099 return Err(self.err(alloc::format!(
15100 "expected ')' to close EXCLUDE, got {:?}",
15101 self.peek()
15102 )));
15103 }
15104 self.advance();
15105 Ok(crate::ast::TableConstraint::Exclude {
15106 name: None,
15107 method,
15108 elements,
15109 })
15110 }
15111
15112 fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15113 self.advance(); // CHECK
15114 if !matches!(self.peek(), Token::LParen) {
15115 return Err(self.err(alloc::format!(
15116 "expected '(' after CHECK, got {:?}",
15117 self.peek()
15118 )));
15119 }
15120 self.advance();
15121 let expr = self.parse_expr(0)?;
15122 if !matches!(self.peek(), Token::RParen) {
15123 return Err(self.err(alloc::format!(
15124 "expected ')' to close CHECK predicate, got {:?}",
15125 self.peek()
15126 )));
15127 }
15128 self.advance();
15129 // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15130 // are no existing rows for PG to skip, so it rejects the suffix.
15131 Ok(crate::ast::TableConstraint::Check {
15132 name: None,
15133 expr,
15134 not_valid: false,
15135 })
15136 }
15137
15138 /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15139 fn peek_table_level_check_start(&self) -> bool {
15140 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15141 }
15142
15143 /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15144 /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15145 /// on the dedicated FK path (`parse_table_level_fk` consumes its
15146 /// own CONSTRAINT prefix).
15147 fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15148 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15149 return None;
15150 }
15151 // tokens[pos+1] is the constraint name (any ident-like);
15152 // tokens[pos+2] is the kind keyword.
15153 match self.tokens.get(self.pos + 2) {
15154 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15155 Some(NamedTableConstraintKind::Check)
15156 }
15157 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15158 Some(NamedTableConstraintKind::Unique)
15159 }
15160 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15161 Some(NamedTableConstraintKind::PrimaryKey)
15162 }
15163 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15164 Some(NamedTableConstraintKind::Exclude)
15165 }
15166 _ => None,
15167 }
15168 }
15169
15170 fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15171 if !matches!(self.peek(), Token::LParen) {
15172 return Err(self.err(alloc::format!(
15173 "expected '(' after {ctx}, got {:?}",
15174 self.peek()
15175 )));
15176 }
15177 self.advance();
15178 let mut out = Vec::new();
15179 loop {
15180 out.push(self.expect_ident_like()?);
15181 match self.peek() {
15182 Token::Comma => {
15183 self.advance();
15184 }
15185 Token::RParen => {
15186 self.advance();
15187 break;
15188 }
15189 other => {
15190 return Err(self.err(alloc::format!(
15191 "expected ',' or ')' in {ctx} list, got {other:?}"
15192 )));
15193 }
15194 }
15195 }
15196 if out.is_empty() {
15197 return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15198 }
15199 Ok(out)
15200 }
15201
15202 /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15203 /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15204 /// table-level FK; a column def never starts with either keyword
15205 /// (column names are not in this reserved set).
15206 fn peek_constraint_or_fk_start(&self) -> bool {
15207 let is_constraint_kw = matches!(
15208 self.peek(),
15209 Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15210 );
15211 let is_foreign_kw = matches!(
15212 self.peek(),
15213 Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15214 );
15215 is_constraint_kw || is_foreign_kw
15216 }
15217
15218 /// v7.6.0 — parse a table-level FK clause:
15219 /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15220 /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15221 fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15222 let mut name: Option<String> = None;
15223 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15224 self.advance();
15225 name = Some(self.expect_ident_like()?);
15226 }
15227 // `FOREIGN`
15228 match self.advance() {
15229 Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15230 other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15231 }
15232 // `KEY`
15233 match self.advance() {
15234 Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15235 other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15236 }
15237 // `(col, col, ...)`
15238 if !matches!(self.peek(), Token::LParen) {
15239 return Err(self.err(format!(
15240 "expected '(' after FOREIGN KEY, got {:?}",
15241 self.peek()
15242 )));
15243 }
15244 self.advance();
15245 let mut columns = Vec::new();
15246 loop {
15247 columns.push(self.expect_ident_like()?);
15248 match self.peek() {
15249 Token::Comma => {
15250 self.advance();
15251 }
15252 Token::RParen => {
15253 self.advance();
15254 break;
15255 }
15256 other => {
15257 return Err(self.err(format!(
15258 "expected ',' or ')' in FK column list, got {other:?}"
15259 )));
15260 }
15261 }
15262 }
15263 if columns.is_empty() {
15264 return Err(self.err("FOREIGN KEY requires at least one column".into()));
15265 }
15266 let (
15267 parent_table,
15268 parent_columns,
15269 on_delete,
15270 on_update,
15271 match_type,
15272 deferrable,
15273 initially_deferred,
15274 ) = self.parse_references_tail(columns.len())?;
15275 Ok(ForeignKeyConstraint {
15276 name,
15277 columns,
15278 parent_table,
15279 parent_columns,
15280 on_delete,
15281 on_update,
15282 match_type,
15283 deferrable,
15284 initially_deferred,
15285 })
15286 }
15287
15288 /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15289 /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15290 /// the local column count, used to default the parent column
15291 /// list when omitted (SQL spec: parent's PK is implied).
15292 fn parse_references_tail(
15293 &mut self,
15294 expected_arity: usize,
15295 ) -> Result<
15296 (
15297 String,
15298 Vec<String>,
15299 FkAction,
15300 FkAction,
15301 crate::ast::MatchType,
15302 // v7.39 (round 288) — deferrable, initially_deferred.
15303 bool,
15304 bool,
15305 ),
15306 ParseError,
15307 > {
15308 match self.advance() {
15309 Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15310 other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15311 }
15312 let parent_table = self.expect_ident_like()?;
15313 let mut parent_columns: Vec<String> = Vec::new();
15314 if matches!(self.peek(), Token::LParen) {
15315 self.advance();
15316 loop {
15317 parent_columns.push(self.expect_ident_like()?);
15318 match self.peek() {
15319 Token::Comma => {
15320 self.advance();
15321 }
15322 Token::RParen => {
15323 self.advance();
15324 break;
15325 }
15326 other => {
15327 return Err(self.err(format!(
15328 "expected ',' or ')' in REFERENCES column list, got {other:?}"
15329 )));
15330 }
15331 }
15332 }
15333 }
15334 if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15335 return Err(self.err(format!(
15336 "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15337 expected_arity,
15338 parent_columns.len()
15339 )));
15340 }
15341 // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15342 // it between the referenced column list and the ON / DEFERRABLE
15343 // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15344 // is skipped when any referencing column is NULL), so SIMPLE —
15345 // the default, and the only spelling pg_dump emits — is accepted
15346 // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15347 // mixed-NULL rule, which is not wired yet; reject them honestly
15348 // rather than silently applying SIMPLE (PG itself errors on
15349 // MATCH PARTIAL as "not yet implemented").
15350 let mut match_type = crate::ast::MatchType::Simple;
15351 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15352 self.advance();
15353 // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15354 // SIMPLE / PARTIAL arrive as bare identifiers.
15355 let kind = match self.advance() {
15356 Token::Full => "FULL".to_string(),
15357 Token::Ident(s) => s.to_uppercase(),
15358 other => {
15359 return Err(self.err(format!(
15360 "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15361 )));
15362 }
15363 };
15364 match kind.as_str() {
15365 "SIMPLE" => {} // Default — match_type stays Simple.
15366 // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15367 // when ALL referencing columns are NULL; a mixed-NULL key errors.
15368 "FULL" => match_type = crate::ast::MatchType::Full,
15369 "PARTIAL" => {
15370 return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15371 }
15372 _ => {
15373 return Err(self.err(format!(
15374 "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15375 )));
15376 }
15377 }
15378 }
15379 // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15380 // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15381 // <action>` / `ON UPDATE <action>` in either order. PG /
15382 // pg_dump emits the timing clause AFTER the ON clauses
15383 // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15384 // but the SQL spec allows either order. We loop over
15385 // every possible trailer and dispatch on the next token,
15386 // stopping when nothing matches. Phase 3.1 changes the
15387 // bare DEFERRABLE form from hard-error to accept-as-
15388 // immediate; SPG is single-writer with no deferred-
15389 // constraint window so the runtime semantics are always
15390 // immediate even when INITIALLY DEFERRED is requested.
15391 // PG's default referential action (no ON DELETE / ON UPDATE
15392 // clause) is NO ACTION, not RESTRICT — the two enforce
15393 // identically in SPG (single-writer, no deferred window; see the
15394 // shared match arm in constraints.rs) but information_schema.
15395 // referential_constraints must report NO ACTION to match PG.
15396 let mut on_delete = FkAction::NoAction;
15397 let mut on_update = FkAction::NoAction;
15398 let mut seen_on_delete = false;
15399 let mut seen_on_update = false;
15400 let mut deferrable = false;
15401 let mut initially_deferred = false;
15402 loop {
15403 // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15404 let before = self.pos;
15405 let (d, idef) = self.consume_deferrable_clauses_timed()?;
15406 if self.pos != before {
15407 deferrable = d;
15408 initially_deferred = idef;
15409 continue;
15410 }
15411 // ON DELETE / ON UPDATE.
15412 if !matches!(self.peek(), Token::On) {
15413 break;
15414 }
15415 self.advance();
15416 let which = self.advance();
15417 let action = self.parse_fk_action()?;
15418 match which {
15419 Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15420 if seen_on_delete {
15421 return Err(self.err("ON DELETE specified twice".into()));
15422 }
15423 seen_on_delete = true;
15424 on_delete = action;
15425 }
15426 Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15427 if seen_on_update {
15428 return Err(self.err("ON UPDATE specified twice".into()));
15429 }
15430 seen_on_update = true;
15431 on_update = action;
15432 }
15433 other => {
15434 return Err(
15435 self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15436 );
15437 }
15438 }
15439 }
15440 Ok((
15441 parent_table,
15442 parent_columns,
15443 on_delete,
15444 on_update,
15445 match_type,
15446 deferrable,
15447 initially_deferred,
15448 ))
15449 }
15450
15451 /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15452 /// NO ACTION`.
15453 fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15454 match self.advance() {
15455 Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15456 Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15457 Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15458 Token::Null => Ok(FkAction::SetNull),
15459 Token::Default => Ok(FkAction::SetDefault),
15460 other => Err(self.err(format!(
15461 "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15462 ))),
15463 },
15464 Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15465 Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15466 other => Err(self.err(format!(
15467 "expected ACTION after NO in FK action, got {other:?}"
15468 ))),
15469 },
15470 other => Err(self.err(format!(
15471 "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15472 ))),
15473 }
15474 }
15475
15476 /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15477 /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15478 fn consume_if_not_exists(&mut self) -> bool {
15479 // `IF` arrives as a bare Ident (we don't reserve it because it
15480 // also appears mid-expression in PG, though we don't support
15481 // those forms yet).
15482 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15483 if !looks_like_if {
15484 return false;
15485 }
15486 // Peek one ahead before committing: only consume IF when it's
15487 // actually `IF NOT EXISTS`.
15488 if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
15489 return false;
15490 }
15491 if !matches!(
15492 self.tokens.get(self.pos + 2),
15493 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15494 ) {
15495 return false;
15496 }
15497 self.advance(); // IF
15498 self.advance(); // NOT
15499 self.advance(); // EXISTS
15500 true
15501 }
15502
15503 /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
15504 /// Consumes IF EXISTS as a pair; returns false otherwise
15505 /// without consuming any tokens.
15506 /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
15507 /// ENABLE/DISABLE/FORCE/NO FORCE.
15508 fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
15509 for kw in ["row", "level", "security"] {
15510 if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
15511 {
15512 return Err(self.err(alloc::format!(
15513 "expected {} in ROW LEVEL SECURITY, got {:?}",
15514 kw.to_ascii_uppercase(),
15515 self.peek()
15516 )));
15517 }
15518 self.advance();
15519 }
15520 Ok(())
15521 }
15522
15523 fn consume_if_exists(&mut self) -> bool {
15524 let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15525 if !looks_like_if {
15526 return false;
15527 }
15528 if !matches!(
15529 self.tokens.get(self.pos + 1),
15530 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
15531 ) {
15532 return false;
15533 }
15534 self.advance(); // IF
15535 self.advance(); // EXISTS
15536 true
15537 }
15538
15539 /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
15540 /// qualifiers after an index column ref. ASC / DESC are
15541 /// reserved tokens; NULLS / FIRST / LAST are bare idents.
15542 /// We accept and discard them since single-column BTree
15543 /// stores rows in natural key order today.
15544 /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
15545 /// ORDER BY key. Returns None when absent.
15546 fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
15547 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15548 return Ok(None);
15549 }
15550 self.advance();
15551 match self.advance() {
15552 Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
15553 Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
15554 other => Err(self.err(alloc::format!(
15555 "expected FIRST or LAST after NULLS, got {other:?}"
15556 ))),
15557 }
15558 }
15559
15560 /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
15561 /// rather than discarded.
15562 ///
15563 /// SPG's index does not scan in a direction — column ordering is
15564 /// intrinsic to the storage — but `pg_indexes.indexdef` is a
15565 /// reproduction of the DDL, and dropping the clause meant
15566 /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
15567 /// dump lost it, and a schema diff saw drift on every run.
15568 fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
15569 let mut order = crate::ast::IndexColumnOrder::default();
15570 loop {
15571 match self.peek() {
15572 Token::Asc => {
15573 self.advance();
15574 }
15575 Token::Desc => {
15576 order.descending = true;
15577 self.advance();
15578 }
15579 Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
15580 let look = self.tokens.get(self.pos + 1);
15581 if matches!(
15582 look,
15583 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
15584 || k.eq_ignore_ascii_case("last")
15585 ) {
15586 self.advance();
15587 order.nulls_first = Some(matches!(
15588 self.advance(),
15589 Token::Ident(k) if k.eq_ignore_ascii_case("first")
15590 ));
15591 } else {
15592 break;
15593 }
15594 }
15595 _ => break,
15596 }
15597 }
15598 order
15599 }
15600
15601 fn parse_create_index_stmt_after_create(
15602 &mut self,
15603 is_unique: bool,
15604 ) -> Result<Statement, ParseError> {
15605 // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
15606 debug_assert!(matches!(self.peek(), Token::Index));
15607 self.advance();
15608 // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
15609 // SPG's CREATE INDEX is synchronous end-to-end today (real
15610 // CONCURRENTLY variant with restartable scans queues with
15611 // v7.39 indexes epic), so the modifier has no runtime effect
15612 // — same accept-and-no-op shape as v7.37.16.5 DETACH
15613 // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
15614 // VIEW CONCURRENTLY.
15615 let mut concurrently = false;
15616 if matches!(
15617 self.peek(),
15618 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
15619 ) {
15620 self.advance();
15621 concurrently = true;
15622 }
15623 let if_not_exists = self.consume_if_not_exists();
15624 // v7.39 (read01 round 93) — the index name is optional (PG since
15625 // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
15626 // When the token after `[IF NOT EXISTS]` is already `ON`, no name
15627 // was given; leave it empty and the engine derives a PG-style
15628 // `<table>_<cols>_idx` name at CREATE time (with collision counter).
15629 let name = if matches!(self.peek(), Token::On) {
15630 String::new()
15631 } else {
15632 self.expect_ident_like()?
15633 };
15634 if !matches!(self.peek(), Token::On) {
15635 return Err(self.err(format!(
15636 "expected ON after CREATE INDEX <name>, got {:?}",
15637 self.peek()
15638 )));
15639 }
15640 self.advance();
15641 let table = self.expect_ident_like()?;
15642 // Optional `USING <method>` — only recognised method in v2.0 is
15643 // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
15644 // ident `using` (we don't promote it to a reserved keyword
15645 // because it isn't reserved anywhere else in our SQL surface).
15646 let mut method_name: Option<String> = None;
15647 let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15648 self.advance();
15649 let m = self.expect_ident_like()?;
15650 method_name = Some(m.to_ascii_lowercase());
15651 match m.to_ascii_lowercase().as_str() {
15652 "hnsw" => IndexMethod::Hnsw,
15653 "btree" => IndexMethod::BTree,
15654 "brin" => IndexMethod::Brin,
15655 // v7.12.3 — real GIN inverted index over `tsvector`.
15656 // v7.9.26b's `USING gin` → BTree silent fallback is
15657 // gone; the engine validates that the indexed column
15658 // is `tsvector` at CREATE INDEX time.
15659 "gin" => IndexMethod::Gin,
15660 // v7.9.26b — PG `pg_dump` emits `USING gist` /
15661 // `USING spgist` / `USING hash` for their built-in
15662 // AMs that SPG doesn't have a matching
15663 // implementation for; degrade to BTree on the
15664 // leading column so the schema loads + the index
15665 // catalogue stays consistent. Operator pays the
15666 // planner cost only for the queries that would have
15667 // used the specialised AM.
15668 "gist" | "spgist" | "hash" => IndexMethod::BTree,
15669 // v7.11.3 — pgvector ships both `ivfflat` and
15670 // `hnsw`. Customers shouldn't have to choose
15671 // their on-disk index method based on what SPG
15672 // implements; accept `ivfflat` as a synonym for
15673 // `hnsw` so PG schemas using either method drop
15674 // in. The vector distance op (`<->` / `<#>` /
15675 // `<=>`) at query time still picks the metric.
15676 "ivfflat" => IndexMethod::Hnsw,
15677 other => {
15678 return Err(self.err(alloc::format!(
15679 "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
15680 )));
15681 }
15682 }
15683 } else {
15684 IndexMethod::BTree
15685 };
15686 if !matches!(self.peek(), Token::LParen) {
15687 return Err(self.err(format!(
15688 "expected '(' before indexed column, got {:?}",
15689 self.peek()
15690 )));
15691 }
15692 self.advance();
15693 // v6.8.2 — accept either a bare column ident (legacy) or
15694 // an expression `fn(col, …)` for expression indexes.
15695 // Distinguish by peeking the token *after* the current
15696 // ident: `ident )` is the legacy column-only path;
15697 // anything else triggers the Pratt expression parser.
15698 // (`advance()` uses `mem::replace` to nil out the current
15699 // slot, so we can't save+rewind cleanly — peek-ahead via
15700 // direct index avoids the mutation.)
15701 let mut opclass: Option<String> = None;
15702 let mut key_collation: Option<String> = None;
15703 let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
15704 // Single column with `)` immediately after — fast path.
15705 // v7.9.29 — also: bare column followed by `,` (the
15706 // multi-column form `(a, b, c)`). Without this branch
15707 // the leading ident gets pulled into `parse_expr`
15708 // which then sets `expression = Some(Column(a))` and
15709 // breaks Display round-trip on the multi-column shape.
15710 Token::Ident(s) | Token::QuotedIdent(s)
15711 if matches!(
15712 self.tokens.get(self.pos + 1),
15713 Some(Token::RParen | Token::Comma)
15714 ) =>
15715 {
15716 self.advance();
15717 (s, None)
15718 }
15719 // v7.9.22 — single column followed by a pgvector
15720 // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
15721 // v7.15.0 — capture the opclass instead of discarding
15722 // it so the engine can dispatch (e.g. `gin_trgm_ops`
15723 // → real trigram-shingle GIN over a TEXT column).
15724 // Vector/HNSW opclasses still take their distance
15725 // metric from the query operator (`<->` / `<#>` /
15726 // `<=>`), so for those callers the opclass stays
15727 // informational.
15728 // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
15729 // opclass: `(embedding public.vector_cosine_ops)`. Strip
15730 // the schema and dispatch on the bare opclass, the same
15731 // treatment table/type names get.
15732 Token::Ident(s) | Token::QuotedIdent(s)
15733 if matches!(
15734 self.tokens.get(self.pos + 1),
15735 Some(Token::Ident(_) | Token::QuotedIdent(_))
15736 ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
15737 && matches!(
15738 self.tokens.get(self.pos + 3),
15739 Some(Token::Ident(op) | Token::QuotedIdent(op))
15740 if is_vector_opclass_name(op)
15741 ) =>
15742 {
15743 self.advance(); // column name
15744 self.advance(); // schema qualifier
15745 self.advance(); // dot
15746 let op_tok = self.advance();
15747 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15748 opclass = Some(op.to_ascii_lowercase());
15749 }
15750 (s, None)
15751 }
15752 // r1038 — an operator class is recognised by its POSITION, not
15753 // by a list of names. It used to be `is_vector_opclass_name`,
15754 // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
15755 // sentori's migration wrote — was a syntax error while
15756 // `USING gin (doc)` parsed. Anything sitting between a column
15757 // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
15758 // two bare identifiers in a row are not valid there otherwise.
15759 Token::Ident(s) | Token::QuotedIdent(s)
15760 if matches!(
15761 self.tokens.get(self.pos + 1),
15762 Some(Token::Ident(op) | Token::QuotedIdent(op))
15763 if is_vector_opclass_name(op) || Self::opclass_position_follows(
15764 self.tokens.get(self.pos + 2)
15765 )
15766 ) =>
15767 {
15768 self.advance(); // column name
15769 // Capture the opclass token, lower-cased for
15770 // case-insensitive engine dispatch.
15771 let op_tok = self.advance();
15772 if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
15773 opclass = Some(op.to_ascii_lowercase());
15774 }
15775 (s, None)
15776 }
15777 Token::Ident(_) | Token::QuotedIdent(_) => {
15778 // v7.39 (round 538) — an explicit COLLATE on the key,
15779 // read by LOOKAHEAD because `parse_expr` absorbs the
15780 // clause as a no-op (SPG orders text by bytes, which is
15781 // the C collation, so it changes nothing to honour). PG
15782 // still PRINTS it: an explicitly written `"C"` and the
15783 // collation a column inherits are different collation
15784 // OBJECTS even where they sort identically, which is why
15785 // `(a COLLATE "C")` shows on a C-collation database too.
15786 if matches!(
15787 self.tokens.get(self.pos + 1),
15788 Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
15789 ) {
15790 key_collation = match self.tokens.get(self.pos + 2) {
15791 Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
15792 Some(n.clone())
15793 }
15794 _ => None,
15795 };
15796 }
15797 let key_expr = self.parse_expr(0)?;
15798 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15799 self.err("expression index key must reference at least one column".into())
15800 })?;
15801 (primary, Some(key_expr))
15802 }
15803 // v7.37.43-T4 — parenthesised expression index key
15804 // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
15805 // PG's CREATE INDEX requires the expression to be in
15806 // its own parens to disambiguate function calls from
15807 // column lists, so this `LParen` is the inner open-paren
15808 // of an expression key. parse_expr handles the recursive
15809 // descent and consumes the matching `RParen`.
15810 Token::LParen => {
15811 let key_expr = self.parse_expr(0)?;
15812 let primary = extract_first_column(&key_expr).ok_or_else(|| {
15813 self.err("expression index key must reference at least one column".into())
15814 })?;
15815 (primary, Some(key_expr))
15816 }
15817 other => {
15818 return Err(self.err(format!(
15819 "expected column ident or expression, got {other:?}"
15820 )));
15821 }
15822 };
15823 // v7.9.14 — accept extra comma-separated columns inside
15824 // the index key parens (`CREATE INDEX … (a, b, c)`).
15825 // mailrs F2. Each extra column may carry an optional
15826 // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
15827 // — parsed and discarded; SPG doesn't honour direction
15828 // on a BTree index today (column ordering is intrinsic
15829 // to the storage). v7.10 will widen to genuine composite
15830 // index keys.
15831 let mut extra_columns: Vec<String> = Vec::new();
15832 // The leading column may also have ASC/DESC after it — and that
15833 // one is the column SPG indexes, so its clause is kept.
15834 let key_order = self.consume_optional_index_column_qualifiers();
15835 while matches!(self.peek(), Token::Comma) {
15836 self.advance();
15837 let extra = self.expect_ident_like()?;
15838 let _ = self.consume_optional_index_column_qualifiers();
15839 extra_columns.push(extra);
15840 }
15841 if !matches!(self.peek(), Token::RParen) {
15842 return Err(self.err(format!(
15843 "expected ')' after indexed column / expression, got {:?}",
15844 self.peek()
15845 )));
15846 }
15847 self.advance();
15848 // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
15849 // index-only-scan annotation. Bare ident (not a reserved
15850 // keyword) so we test by case-insensitive string match.
15851 let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
15852 {
15853 self.advance();
15854 if !matches!(self.peek(), Token::LParen) {
15855 return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
15856 }
15857 self.advance();
15858 let mut cols = Vec::new();
15859 loop {
15860 cols.push(self.expect_ident_like()?);
15861 match self.peek() {
15862 Token::Comma => {
15863 self.advance();
15864 }
15865 Token::RParen => {
15866 self.advance();
15867 break;
15868 }
15869 other => {
15870 return Err(self.err(format!(
15871 "expected ',' or ')' in INCLUDE list, got {other:?}"
15872 )));
15873 }
15874 }
15875 }
15876 cols
15877 } else {
15878 Vec::new()
15879 };
15880 // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
15881 // storage parameters. pgvector emits `WITH (lists = N)` for
15882 // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
15883 // SPG's HNSW picks its own parameters today (tunable via
15884 // env vars), so the WITH clause is informational and dropped.
15885 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15886 self.advance();
15887 if !matches!(self.peek(), Token::LParen) {
15888 return Err(self.err(format!(
15889 "expected '(' after WITH in CREATE INDEX, got {:?}",
15890 self.peek()
15891 )));
15892 }
15893 self.advance();
15894 loop {
15895 if matches!(self.peek(), Token::RParen) {
15896 self.advance();
15897 break;
15898 }
15899 // Drain `key = value` or bare `key` tokens.
15900 let _ = self.advance(); // key
15901 if matches!(self.peek(), Token::Eq) {
15902 self.advance();
15903 let _ = self.advance(); // value (int / string / ident)
15904 }
15905 match self.peek() {
15906 Token::Comma => {
15907 self.advance();
15908 }
15909 Token::RParen => {
15910 self.advance();
15911 break;
15912 }
15913 other => {
15914 return Err(self.err(format!(
15915 "expected ',' or ')' in WITH (…) clause, got {other:?}"
15916 )));
15917 }
15918 }
15919 }
15920 }
15921 // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
15922 // which sits between the key list and the WHERE clause.
15923 let mut nulls_not_distinct = false;
15924 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15925 let n1 = self.tokens.get(self.pos + 1);
15926 let n2 = self.tokens.get(self.pos + 2);
15927 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
15928 self.advance(); // NULLS
15929 self.advance(); // NOT
15930 self.advance(); // DISTINCT
15931 nulls_not_distinct = true;
15932 } else if matches!(n1, Some(Token::Distinct)) {
15933 self.advance(); // NULLS
15934 self.advance(); // DISTINCT
15935 }
15936 }
15937 // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
15938 let partial_predicate = if matches!(self.peek(), Token::Where) {
15939 self.advance();
15940 Some(self.parse_expr(0)?)
15941 } else {
15942 None
15943 };
15944 // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
15945 // sense: uniqueness over an ANN structure has no clean
15946 // semantics. Reject early. (BRIN UNIQUE is similarly
15947 // meaningless — block both.)
15948 if is_unique && !matches!(method, IndexMethod::BTree) {
15949 return Err(self.err(alloc::format!(
15950 "UNIQUE is only supported on BTree indexes, got USING {:?}",
15951 method
15952 )));
15953 }
15954 Ok(Statement::CreateIndex(CreateIndexStatement {
15955 concurrently,
15956 name,
15957 key_order,
15958 key_collation,
15959 table,
15960 column,
15961 nulls_not_distinct,
15962 method,
15963 if_not_exists,
15964 included_columns,
15965 partial_predicate,
15966 extra_columns: extra_columns.clone(),
15967 expression,
15968 is_unique,
15969 opclass,
15970 method_name,
15971 }))
15972 }
15973
15974 /// v7.6.0 — wraps `parse_column_def` and consumes an optional
15975 /// column-level `REFERENCES ...` clause. The trailing FK is
15976 /// normalised into table-level shape (single-element columns +
15977 /// parent_columns) so the engine sees one uniform constraint list.
15978 fn parse_column_def_with_fk(
15979 &mut self,
15980 ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
15981 let col = self.parse_column_def()?;
15982 // v7.39 (round 308, V29) — an explicitly named inline FK:
15983 // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
15984 // loop leaves this spelling intact precisely so the name can be
15985 // kept here; PG reports it in violation messages and matches it
15986 // in `SET CONSTRAINTS`.
15987 let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
15988 {
15989 self.advance();
15990 Some(self.expect_ident_like()?)
15991 } else {
15992 None
15993 };
15994 // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
15995 let inline_references = matches!(
15996 self.peek(),
15997 Token::Ident(s) if s.eq_ignore_ascii_case("references")
15998 );
15999 if !inline_references {
16000 return Ok((col, None));
16001 }
16002 let (
16003 parent_table,
16004 parent_columns,
16005 on_delete,
16006 on_update,
16007 match_type,
16008 deferrable,
16009 initially_deferred,
16010 ) = self.parse_references_tail(1)?;
16011 let fk = ForeignKeyConstraint {
16012 name: declared_name,
16013 columns: vec![col.name.clone()],
16014 parent_table,
16015 parent_columns,
16016 on_delete,
16017 on_update,
16018 match_type,
16019 deferrable,
16020 initially_deferred,
16021 };
16022 Ok((col, Some(fk)))
16023 }
16024
16025 /// v7.13.0 — parse a column type (consuming the type ident and
16026 /// any trailing parameters / `[]`), without surrounding column
16027 /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16028 /// Returns the resolved `ColumnTypeName` plus implied
16029 /// `(auto_increment, not_null)` flags from PG SERIAL family
16030 /// shorthands — callers that don't expect those (ALTER COLUMN
16031 /// TYPE) can discard them.
16032 fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16033 let (ty, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16034 Ok(ty)
16035 }
16036
16037 #[allow(clippy::type_complexity)]
16038 fn parse_type_with_implied_flags(
16039 &mut self,
16040 ) -> Result<
16041 (
16042 ColumnTypeName,
16043 bool,
16044 bool,
16045 Option<String>,
16046 Collation,
16047 // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16048 bool,
16049 // v7.39 (round 676) — the collation NAME as written, which the
16050 // `Collation` enum above cannot carry.
16051 Option<String>,
16052 bool,
16053 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16054 // list captured at type-parse time. None for all
16055 // non-ENUM types.
16056 Option<Vec<String>>,
16057 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16058 // list. Distinct from ENUM (subset semantics).
16059 Option<Vec<String>>,
16060 // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16061 // width, lost when the type collapses to SmallInt / Int.
16062 Option<MysqlIntWidth>,
16063 // v7.39 (round 424) — declared fractional-seconds precision of a
16064 // MySQL temporal column (bare spelling = 0). None under PG.
16065 Option<u8>,
16066 ),
16067 ParseError,
16068 > {
16069 let mut ty_ident = match self.advance() {
16070 Token::Ident(s) => s,
16071 // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16072 // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16073 // '<span>'` literal grammar. As a column type it lands
16074 // here directly; downstream resolution still uses the
16075 // canonical lowercase string.
16076 Token::Interval => "interval".to_string(),
16077 other => {
16078 return Err(ParseError {
16079 message: format!("expected column type, got {other:?}"),
16080 token_pos: self.consumed_pos(),
16081 });
16082 }
16083 };
16084 // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16085 // pg_dump qualifies extension types (`public.vector(1024)`).
16086 // SPG is single-namespace; drop the schema and resolve the
16087 // bare type — same treatment table names already get.
16088 while matches!(self.peek(), Token::Dot) {
16089 self.advance();
16090 ty_ident = self.expect_ident_like()?;
16091 }
16092 let mut implied_auto_increment = false;
16093 let mut implied_not_null = false;
16094 let mut user_type_ref: Option<String> = None;
16095 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16096 // value list, captured here and bubbled up through the
16097 // ColumnDef so the engine can attach it to the column
16098 // schema (and validate INSERT cells against it).
16099 let mut inline_enum_variants: Option<Vec<String>> = None;
16100 // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16101 let mut inline_set_variants: Option<Vec<String>> = None;
16102 // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16103 // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16104 // collapses to SmallInt / Int. Only under the MySQL dialect.
16105 let mut mysql_int_width: Option<MysqlIntWidth> = None;
16106 // v7.39 (round 424) — the declared fractional-seconds precision of a
16107 // MySQL temporal column. Set by the temporal arms below; stays None
16108 // for PG (whose temporal columns keep full microseconds).
16109 let mut mysql_fsp: Option<u8> = None;
16110 let mut ty = match ty_ident.as_str() {
16111 // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16112 "smallserial" | "serial2" => {
16113 implied_auto_increment = true;
16114 implied_not_null = true;
16115 ColumnTypeName::SmallInt
16116 }
16117 "serial" | "serial4" => {
16118 implied_auto_increment = true;
16119 implied_not_null = true;
16120 ColumnTypeName::Int
16121 }
16122 "bigserial" | "serial8" => {
16123 implied_auto_increment = true;
16124 implied_not_null = true;
16125 ColumnTypeName::BigInt
16126 }
16127 // MySQL flavours we accept by aliasing to the closest SPG
16128 // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16129 // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16130 // 24-bit) → INT. UNSIGNED modifiers are consumed below
16131 // without semantic effect.
16132 // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16133 // PG's internal type names; pg_dump and hand-written PG schemas
16134 // use them interchangeably with smallint / int / bigint (the cast
16135 // path already accepted them, only the column grammar didn't).
16136 "smallint" | "int2" => {
16137 // v7.14.0 — MySQL display-width on integers
16138 // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16139 // parenthesised number is purely cosmetic — it
16140 // doesn't change storage. Accept + discard.
16141 self.consume_optional_paren_size();
16142 ColumnTypeName::SmallInt
16143 }
16144 // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16145 // canonical encoding for BOOLEAN. Every MySQL driver
16146 // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16147 // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16148 // 4.3 SPG classified TINYINT(1) as SmallInt, which
16149 // gave the customer i16-shaped values where the app
16150 // expected bool — a Tier-A silent type drift on
16151 // mysqldump restores. Now: `TINYINT(1)` → Bool;
16152 // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16153 // stay SmallInt (the legacy width-agnostic path).
16154 "tinyint" => {
16155 let width = self.peek_optional_paren_size_value();
16156 self.consume_optional_paren_size();
16157 if width == Some(1) {
16158 ColumnTypeName::Bool
16159 } else {
16160 // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16161 // lost width so the write path can enforce -128..127.
16162 if self.mysql_dialect {
16163 mysql_int_width = Some(MysqlIntWidth::Tiny);
16164 }
16165 ColumnTypeName::SmallInt
16166 }
16167 }
16168 "mediumint" => {
16169 self.consume_optional_paren_size();
16170 // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16171 if self.mysql_dialect {
16172 mysql_int_width = Some(MysqlIntWidth::Medium);
16173 }
16174 ColumnTypeName::Int
16175 }
16176 "int" | "integer" | "int4" => {
16177 self.consume_optional_paren_size();
16178 ColumnTypeName::Int
16179 }
16180 "bigint" | "int8" => {
16181 self.consume_optional_paren_size();
16182 ColumnTypeName::BigInt
16183 }
16184 // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16185 // (mailrs round-5 G6). Consume the optional `PRECISION`
16186 // tail when the type keyword was `double` / `DOUBLE`.
16187 //
16188 // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16189 // FLOAT". `FLOAT(p)` picks the width the way PG does:
16190 // p in 1..=24 is real, 25..=53 is double precision, and
16191 // anything else is an error.
16192 "float" | "double" | "real" => {
16193 if ty_ident.eq_ignore_ascii_case("double")
16194 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16195 {
16196 self.advance();
16197 }
16198 if ty_ident.eq_ignore_ascii_case("real") {
16199 // v7.39 (round 274) — the two dialects genuinely
16200 // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16201 // synonym for DOUBLE (8-byte). Round 269 made REAL
16202 // 32-bit globally and thereby narrowed the stored
16203 // precision of every MySQL REAL column.
16204 if self.mysql_dialect {
16205 ColumnTypeName::Float
16206 } else {
16207 ColumnTypeName::Real
16208 }
16209 } else if ty_ident.eq_ignore_ascii_case("float")
16210 && self.mysql_dialect
16211 && matches!(self.peek(), Token::LParen)
16212 && self.peek_paren_has_comma()
16213 {
16214 // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16215 // display form (`FLOAT(10,2)`), which PG has no
16216 // equivalent of. It was `syntax error at or near ","`,
16217 // so the whole CREATE failed. The digits are a display
16218 // hint only; SPG stores the full double.
16219 self.consume_optional_paren_size();
16220 ColumnTypeName::Float
16221 } else if ty_ident.eq_ignore_ascii_case("float")
16222 && matches!(self.peek(), Token::LParen)
16223 {
16224 // PG words the two bounds differently, and
16225 // parse_paren_size already rejects a zero.
16226 let p = self.parse_paren_size("FLOAT")?;
16227 if p > 53 {
16228 return Err(self.err(String::from(
16229 "precision for type float must be less than 54 bits",
16230 )));
16231 }
16232 if p <= 24 {
16233 ColumnTypeName::Real
16234 } else {
16235 ColumnTypeName::Float
16236 }
16237 } else {
16238 ColumnTypeName::Float
16239 }
16240 }
16241 // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16242 "float4" => ColumnTypeName::Real,
16243 "float8" => ColumnTypeName::Float,
16244 "text" => ColumnTypeName::Text,
16245 // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16246 // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16247 // real MySQL schema and NONE of them existed: the CREATE
16248 // failed outright with `type "blob" does not exist`, so the
16249 // table was never made. The sizes differ only in MySQL's
16250 // maximum length, which SPG does not cap, so they collapse
16251 // onto TEXT and BYTEA the way the unsized spellings do.
16252 "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16253 "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16254 // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16255 // enforce, consumed so the declaration parses.
16256 "varbinary" | "binary" => {
16257 self.consume_optional_paren_size();
16258 ColumnTypeName::Bytes
16259 }
16260 "name" => ColumnTypeName::Name,
16261 "xid" => ColumnTypeName::Xid,
16262 "oid" => ColumnTypeName::Oid,
16263 "xid8" => ColumnTypeName::Xid8,
16264 "bool" | "boolean" => ColumnTypeName::Bool,
16265 // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16266 // an unbounded `character varying`, which the arm below has always
16267 // read as text. Only the short spelling demanded a length, so
16268 // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16269 // there is — failed on `VARCHAR type requires (N)` while the long
16270 // spelling of the same thing was accepted. The same asymmetry
16271 // round 613 closed on the CAST side, here on the DDL side.
16272 "varchar" => {
16273 if matches!(self.peek(), Token::LParen) {
16274 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16275 } else {
16276 ColumnTypeName::Text
16277 }
16278 }
16279 // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16280 // `character` below (SQL standard).
16281 "char" => {
16282 if matches!(self.peek(), Token::LParen) {
16283 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16284 } else {
16285 ColumnTypeName::Char(1)
16286 }
16287 }
16288 // pg_dump's canonical spellings: `character varying(n)` = varchar,
16289 // `character(n)` = char, bare `character` = char(1). Unbounded
16290 // `character varying` maps to text.
16291 "character" => {
16292 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16293 self.advance();
16294 if matches!(self.peek(), Token::LParen) {
16295 ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16296 } else {
16297 ColumnTypeName::Text
16298 }
16299 } else if matches!(self.peek(), Token::LParen) {
16300 ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16301 } else {
16302 ColumnTypeName::Char(1)
16303 }
16304 }
16305 "vector" => {
16306 let dim = self.parse_paren_size("VECTOR")?;
16307 let encoding = self.parse_optional_vector_encoding()?;
16308 ColumnTypeName::Vector { dim, encoding }
16309 }
16310 // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16311 // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16312 // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16313 // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16314 // DECIMAL(10,2))` — how nearly every money column is written,
16315 // in either dialect — was a syntax error and the table was
16316 // never created. `FIXED` is MySQL's alias alone, so it is
16317 // taken only in that dialect.
16318 "numeric" | "decimal" | "dec" => {
16319 let (precision, scale) = self.parse_optional_numeric_params()?;
16320 ColumnTypeName::Numeric(precision, scale)
16321 }
16322 "fixed" if self.mysql_dialect => {
16323 let (precision, scale) = self.parse_optional_numeric_params()?;
16324 ColumnTypeName::Numeric(precision, scale)
16325 }
16326 "date" => ColumnTypeName::Date,
16327 // MySQL's `DATETIME` is the same domain as standard
16328 // `TIMESTAMP` — accept both spellings.
16329 "timestamp" | "datetime" => {
16330 // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16331 // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16332 // TIME ZONE` clause, so consume it first.
16333 // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16334 // (it truncates on write and pads on render), so capture it;
16335 // a bare spelling means precision 0 there. PG stores µs always
16336 // and keeps `None`.
16337 let n = self.take_optional_paren_size();
16338 if self.mysql_dialect {
16339 mysql_fsp = Some(n.unwrap_or(0).min(6));
16340 }
16341 // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16342 // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16343 // the full form. SPG canonicalises:
16344 // - WITH TIME ZONE → Timestamptz
16345 // - WITHOUT TIME ZONE → Timestamp
16346 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16347 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16348 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16349 {
16350 self.advance(); // WITH
16351 self.advance(); // TIME
16352 self.advance(); // ZONE
16353 ColumnTypeName::Timestamptz
16354 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16355 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16356 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16357 {
16358 self.advance(); // WITHOUT
16359 self.advance(); // TIME
16360 self.advance(); // ZONE
16361 ColumnTypeName::Timestamp
16362 } else {
16363 // A second `(precision)` cannot legally follow, but the
16364 // old grammar tolerated it; keep that tolerance.
16365 self.consume_optional_paren_size();
16366 ColumnTypeName::Timestamp
16367 }
16368 }
16369 // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16370 // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16371 // only PG-wire OID differs.
16372 "timestamptz" => {
16373 self.consume_optional_paren_size();
16374 ColumnTypeName::Timestamptz
16375 }
16376 // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16377 // validation. We accept the JSONB spelling too because
16378 // most PG clients default to it; SPG doesn't distinguish
16379 // the two (no path-operator perf advantage to model).
16380 "json" => ColumnTypeName::Json,
16381 "jsonb" => ColumnTypeName::Jsonb,
16382 // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16383 // surface here. Same storage shape; mapping happens at
16384 // the engine side via the ColumnTypeName → DataType
16385 // resolver. Literal forms are handled at coerce_value
16386 // time so the lexer stays untouched.
16387 "bytea" | "bytes" => ColumnTypeName::Bytes,
16388 // v7.17.0 Phase 7 — PG network address types
16389 // v7.17.0 had a Text-backed fallback here for
16390 // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16391 // each to a first-class type; the keywords are
16392 // bound below in the ζ-A block.
16393 // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16394 // The actual `to_tsvector` / `@@` / `ts_rank` surface
16395 // arrives in v7.12.1+; the type itself loads here so
16396 // mailrs's `scripts/init-schema.sql` runs unmodified.
16397 "tsvector" => ColumnTypeName::TsVector,
16398 "tsquery" => ColumnTypeName::TsQuery,
16399 // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
16400 // surface for Django / Rails / Hibernate's default
16401 // PK pattern.
16402 "uuid" => ColumnTypeName::Uuid,
16403 // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
16404 // Storage = three-field {months, days, micros}, catalog
16405 // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
16406 // line `INTERVAL` was parser-rejected at CREATE TABLE.
16407 "interval" => {
16408 // pg_dump emits field-qualified forms like `INTERVAL DAY TO
16409 // SECOND` and an optional `(p)` precision. SPG stores the full
16410 // {months,days,micros}; consume + ignore the qualifier/precision.
16411 while matches!(self.peek(), Token::To)
16412 || matches!(self.peek(), Token::Ident(s) if matches!(
16413 s.to_ascii_lowercase().as_str(),
16414 "year" | "month" | "day" | "hour" | "minute" | "second"
16415 ))
16416 {
16417 self.advance();
16418 }
16419 self.consume_optional_paren_size();
16420 ColumnTypeName::Interval
16421 }
16422 // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
16423 // i64 microseconds since 00:00:00. Wire OID 1083.
16424 // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
16425 "time" => {
16426 // v7.39 (round 424) — MySQL TIME carries a semantic
16427 // fractional-seconds precision, bare meaning 0.
16428 let n = self.take_optional_paren_size();
16429 if self.mysql_dialect {
16430 mysql_fsp = Some(n.unwrap_or(0).min(6));
16431 }
16432 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16433 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16434 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16435 {
16436 self.advance();
16437 self.advance();
16438 self.advance();
16439 ColumnTypeName::TimeTz
16440 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16441 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16442 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16443 {
16444 self.advance();
16445 self.advance();
16446 self.advance();
16447 ColumnTypeName::Time
16448 } else {
16449 ColumnTypeName::Time
16450 }
16451 }
16452 // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
16453 // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
16454 "year" => ColumnTypeName::Year,
16455 // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
16456 // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
16457 "timetz" => ColumnTypeName::TimeTz,
16458 // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
16459 // Wire OID 790.
16460 "money" => ColumnTypeName::Money,
16461 // v7.17.0 Phase 3.P0-38 — PG range types.
16462 "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
16463 "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
16464 "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
16465 "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
16466 "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
16467 "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
16468 // v7.37.5 δ — PG 14+ multirange keywords.
16469 "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
16470 "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
16471 "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
16472 "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
16473 "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
16474 "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
16475 // v7.37.5 ε — PG geometry scalar keywords.
16476 "point" => ColumnTypeName::Point,
16477 "lseg" => ColumnTypeName::Lseg,
16478 "path" => ColumnTypeName::Path,
16479 "box" => ColumnTypeName::PgBox,
16480 "polygon" => ColumnTypeName::Polygon,
16481 "line" => ColumnTypeName::Line,
16482 "circle" => ColumnTypeName::Circle,
16483 // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
16484 "inet" => ColumnTypeName::Inet,
16485 "cidr" => ColumnTypeName::Cidr,
16486 "macaddr" => ColumnTypeName::Macaddr,
16487 "macaddr8" => ColumnTypeName::Macaddr8,
16488 // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
16489 // width in the value, so the optional `(N)` typmod is accepted and
16490 // ignored (the column stores whatever width it's given).
16491 "bit" => {
16492 let varying = matches!(
16493 self.peek(),
16494 Token::Ident(k) if k.eq_ignore_ascii_case("varying")
16495 );
16496 if varying {
16497 self.advance();
16498 }
16499 // v7.39 (round 281) — the length used to be parsed and
16500 // dropped, so `bit(3)` accepted a five-bit string.
16501 let n = if matches!(self.peek(), Token::LParen) {
16502 self.parse_paren_size("BIT")?
16503 } else {
16504 0
16505 };
16506 if varying {
16507 ColumnTypeName::BitVarying(n)
16508 } else {
16509 ColumnTypeName::Bit(n)
16510 }
16511 }
16512 "varbit" => {
16513 let n = if matches!(self.peek(), Token::LParen) {
16514 self.parse_paren_size("VARBIT")?
16515 } else {
16516 0
16517 };
16518 ColumnTypeName::BitVarying(n)
16519 }
16520 "xml" => ColumnTypeName::Xml,
16521 // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
16522 "hstore" => ColumnTypeName::Hstore,
16523 // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
16524 // `ENUM('a','b','c')`. Storage is TEXT; the value
16525 // list lands on `inline_enum_variants` for the
16526 // engine to validate INSERT cells against. Empty
16527 // value list is a parse error (matches MySQL).
16528 "enum" => {
16529 // Expect the opening `(`.
16530 if !matches!(self.peek(), Token::LParen) {
16531 return Err(self.err(alloc::format!(
16532 "expected '(' after ENUM, got {:?}",
16533 self.peek()
16534 )));
16535 }
16536 self.advance();
16537 let mut variants: Vec<String> = Vec::new();
16538 loop {
16539 match self.advance() {
16540 Token::String(s) => variants.push(s),
16541 other => {
16542 return Err(self.err(alloc::format!(
16543 "ENUM(...) expects string literal variants, got {other:?}"
16544 )));
16545 }
16546 }
16547 match self.peek() {
16548 Token::Comma => {
16549 self.advance();
16550 continue;
16551 }
16552 Token::RParen => {
16553 self.advance();
16554 break;
16555 }
16556 other => {
16557 return Err(self.err(alloc::format!(
16558 "expected ',' or ')' in ENUM(...), got {other:?}"
16559 )));
16560 }
16561 }
16562 }
16563 if variants.is_empty() {
16564 return Err(self.err("ENUM(...) must declare at least one variant".into()));
16565 }
16566 inline_enum_variants = Some(variants);
16567 // Storage is plain TEXT; the variant list lives on
16568 // the ColumnSchema side.
16569 ColumnTypeName::Text
16570 }
16571 // v7.17.0 Phase 3.P0-37 — MySQL inline SET
16572 // `SET('a','b','c')`. Same parse shape as ENUM;
16573 // semantics differ (subset rather than pick-one).
16574 "set" => {
16575 if !matches!(self.peek(), Token::LParen) {
16576 return Err(self.err(alloc::format!(
16577 "expected '(' after SET, got {:?}",
16578 self.peek()
16579 )));
16580 }
16581 self.advance();
16582 let mut variants: Vec<String> = Vec::new();
16583 loop {
16584 match self.advance() {
16585 Token::String(s) => variants.push(s),
16586 other => {
16587 return Err(self.err(alloc::format!(
16588 "SET(...) expects string literal variants, got {other:?}"
16589 )));
16590 }
16591 }
16592 match self.peek() {
16593 Token::Comma => {
16594 self.advance();
16595 continue;
16596 }
16597 Token::RParen => {
16598 self.advance();
16599 break;
16600 }
16601 other => {
16602 return Err(self.err(alloc::format!(
16603 "expected ',' or ')' in SET(...), got {other:?}"
16604 )));
16605 }
16606 }
16607 }
16608 if variants.is_empty() {
16609 return Err(self.err("SET(...) must declare at least one variant".into()));
16610 }
16611 inline_set_variants = Some(variants);
16612 ColumnTypeName::Text
16613 }
16614 _other => {
16615 // v7.17.0 Phase 1.4 — unknown ident → defer
16616 // resolution to the engine. Stored as Text in
16617 // ColumnTypeName + the original name carried as
16618 // `user_type_ref` so CREATE TABLE can look up
16619 // user-defined enum / domain types.
16620 user_type_ref = Some(ty_ident.clone());
16621 ColumnTypeName::Text
16622 }
16623 };
16624 // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
16625 // right after the type keyword. Pre-4.4 SPG consumed +
16626 // discarded the keyword, leaving a customer column
16627 // declared `id INT UNSIGNED NOT NULL` silently accepting
16628 // negative values — a Tier-A correctness drift where
16629 // application invariants (auto-increment-IDs never
16630 // negative) silently broke on cutover. Now: capture as
16631 // a column flag, persist on the schema, enforce at
16632 // INSERT / UPDATE time.
16633 let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
16634 {
16635 self.advance();
16636 true
16637 } else {
16638 false
16639 };
16640 // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
16641 // `<type> COLLATE <name>` post-fixes on text columns. SPG
16642 // stores text as UTF-8 always so CHARACTER SET is still a
16643 // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
16644 // name: it gets classified into a `Collation` variant the
16645 // engine consults at WHERE-eval time. PG `default` /
16646 // `pg_catalog.default` / `C` / `POSIX` collations all
16647 // resolve to `Binary` (the prior behaviour); `_ci` /
16648 // `case_insensitive` / `nocase` shift to CaseInsensitive.
16649 // The schema-qualifier form (`pg_catalog.default`) lexes
16650 // as `Ident '.' Ident` — peek for the `.` and consume both
16651 // halves so it's treated as one collation name. PG's
16652 // `IDENT.IDENT` collation form (which can appear here) is
16653 // resolved by Collation::from_collation_name on the bare
16654 // identifier after the dot.
16655 let mut collation = Collation::Binary;
16656 // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
16657 // clause was written. The engine needs this to tell an explicit
16658 // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
16659 // clause at all: both resolve to `Collation::Binary`, but under the
16660 // MySQL dialect the latter takes the folding default collation.
16661 let mut collation_explicit = false;
16662 let mut collation_name: Option<alloc::string::String> = None;
16663 loop {
16664 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
16665 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
16666 {
16667 self.advance(); // CHARACTER
16668 self.advance(); // SET
16669 if matches!(
16670 self.peek(),
16671 Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
16672 ) {
16673 self.advance();
16674 }
16675 continue;
16676 }
16677 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
16678 self.advance(); // COLLATE
16679 // Accept Ident / QuotedIdent / String AND the
16680 // keyword-tokenised `Default` (PG `pg_catalog.default`
16681 // and bare `DEFAULT` collation names — `default` is a
16682 // reserved word so the lexer hands back Token::Default
16683 // not Token::Ident).
16684 let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
16685 match this.peek().clone() {
16686 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
16687 this.advance();
16688 Some(s)
16689 }
16690 Token::Default => {
16691 this.advance();
16692 Some(alloc::string::String::from("default"))
16693 }
16694 _ => None,
16695 }
16696 };
16697 let raw = if let Some(head) = read_collation_atom(self) {
16698 // Schema-qualified PG form: `pg_catalog.default`.
16699 if matches!(self.peek(), Token::Dot) {
16700 self.advance();
16701 let tail = read_collation_atom(self).unwrap_or_default();
16702 alloc::format!("{head}.{tail}")
16703 } else {
16704 head
16705 }
16706 } else {
16707 alloc::string::String::new()
16708 };
16709 if !raw.is_empty() {
16710 collation_explicit = true;
16711 // v7.39 (round 676) — keep the name too. The enum below
16712 // folds C / POSIX / en_US / default into one value, and
16713 // `pg_attribute.attcollation` has to tell them apart.
16714 // The schema qualifier goes: PG's `pg_catalog.default`
16715 // and a bare `default` name the same collation.
16716 // v7.39 (round 679) — strip a SCHEMA qualifier, not an
16717 // encoding suffix. Round 676 used `rsplit('.')` for
16718 // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
16719 // PG writes `pg_catalog.default` (qualifier) and
16720 // `en_US.utf8` (locale + encoding) with the same
16721 // separator. Only `pg_catalog.` is a qualifier, and it
16722 // is the only one PG's own dumps emit.
16723 let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
16724 let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
16725 collation_name = Some(alloc::string::String::from(bare));
16726 let parsed = Collation::from_collation_name(&raw);
16727 // Last COLLATE clause wins, but `Binary` from a
16728 // bare keyword like `default` should not
16729 // silently downgrade a stronger one set earlier
16730 // on the same column. v7.17 only ships one
16731 // non-Binary variant so a simple OR is enough.
16732 if parsed != Collation::Binary {
16733 collation = parsed;
16734 }
16735 }
16736 continue;
16737 }
16738 break;
16739 }
16740 // v7.10.10 — postfix `[]` widens the base type to its array
16741 // type. PG accepts `TYPE[]` after any base type and so does
16742 // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
16743 // all through; the old "only TEXT[]" note was stale).
16744 if matches!(self.peek(), Token::LBracket) {
16745 self.advance();
16746 if !matches!(self.peek(), Token::RBracket) {
16747 return Err(self.err(alloc::format!(
16748 "TEXT[] takes no dimension; got {:?}",
16749 self.peek()
16750 )));
16751 }
16752 self.advance();
16753 // v7.11.13 — widened to INT[] and BIGINT[] in addition
16754 // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
16755 // still error here.
16756 ty = match ty {
16757 ColumnTypeName::Text => ColumnTypeName::TextArray,
16758 ColumnTypeName::Int => ColumnTypeName::IntArray,
16759 ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
16760 // v7.37.5 β-P4 — INTERVAL[] via the same postfix
16761 // `[]` grammar. Wire OID 1187.
16762 ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
16763 // v7.37.5 γ — full PG array-of-scalar family.
16764 ColumnTypeName::Bool => ColumnTypeName::BoolArray,
16765 ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
16766 ColumnTypeName::Float => ColumnTypeName::FloatArray,
16767 // NUMERIC(p, s) loses its precision params at the
16768 // array level (matches PG: `NUMERIC[]` is untyped,
16769 // per-element precision flows through values).
16770 ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
16771 ColumnTypeName::Date => ColumnTypeName::DateArray,
16772 ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
16773 ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
16774 ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
16775 ColumnTypeName::Json => ColumnTypeName::JsonArray,
16776 ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
16777 ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
16778 // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
16779 // the array level (matches PG semantics where the
16780 // element precision is per-row, not column-wide).
16781 ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
16782 ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
16783 // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
16784 // follow-up.
16785 ColumnTypeName::Money => ColumnTypeName::MoneyArray,
16786 other => {
16787 return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
16788 }
16789 };
16790 // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
16791 // for INT/TEXT/BIGINT. Anything else is an error.
16792 if matches!(self.peek(), Token::LBracket) {
16793 self.advance();
16794 if !matches!(self.peek(), Token::RBracket) {
16795 return Err(self.err(alloc::format!(
16796 "TYPE[][] second dimension takes no size; got {:?}",
16797 self.peek()
16798 )));
16799 }
16800 self.advance();
16801 ty = match ty {
16802 ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
16803 ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
16804 ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
16805 // v7.39 (read01 round 75) — bool[][].
16806 ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
16807 other => {
16808 return Err(self.err(alloc::format!(
16809 "v7.17 2D arrays support INT[][] / BIGINT[][] / \
16810 TEXT[][] only; got {other:?}"
16811 )));
16812 }
16813 };
16814 }
16815 }
16816 Ok((
16817 ty,
16818 implied_auto_increment,
16819 implied_not_null,
16820 user_type_ref,
16821 collation,
16822 collation_explicit,
16823 collation_name,
16824 is_unsigned,
16825 inline_enum_variants,
16826 inline_set_variants,
16827 mysql_int_width,
16828 mysql_fsp,
16829 ))
16830 }
16831
16832 fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
16833 // v7.20 — PG reserves the table-constraint keywords, so a
16834 // BARE `UNIQUE` / `PRIMARY` / … in column position is a
16835 // malformed constraint clause (e.g. `UNIQUE a` missing its
16836 // parens), not a column named "unique". Since v7.17's
16837 // unknown-type leniency (`user_type_ref`) such a clause
16838 // would otherwise parse as a column with a user-defined
16839 // type — silently accepting invalid DDL. Quoted
16840 // identifiers ("unique" / `unique`) remain valid names.
16841 if let Token::Ident(s) = self.peek()
16842 && [
16843 "unique",
16844 "primary",
16845 "foreign",
16846 "constraint",
16847 "check",
16848 "references",
16849 "exclude",
16850 ]
16851 .iter()
16852 .any(|kw| s.eq_ignore_ascii_case(kw))
16853 {
16854 return Err(self.err(alloc::format!(
16855 "unexpected reserved keyword '{s}' at start of column definition \
16856 (malformed table constraint?)"
16857 )));
16858 }
16859 let name = self.expect_ident_like()?;
16860 let (
16861 ty,
16862 implied_auto_increment,
16863 implied_not_null,
16864 user_type_ref,
16865 collation,
16866 collation_explicit,
16867 collation_name,
16868 is_unsigned,
16869 inline_enum_variants,
16870 inline_set_variants,
16871 mysql_int_width,
16872 mysql_fsp,
16873 ) = self.parse_type_with_implied_flags()?;
16874 // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
16875 // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
16876 // each at most once.
16877 let mut default: Option<Expr> = None;
16878 let mut nullable = !implied_not_null;
16879 let mut nullability_seen = implied_not_null;
16880 let mut auto_increment = implied_auto_increment;
16881 let mut is_primary_key = false;
16882 let mut is_unique = false;
16883 let mut unique_nulls_not_distinct = false;
16884 let mut constraint_deferrable = false;
16885 let mut constraint_initially_deferred = false;
16886 let mut check: Option<Expr> = None;
16887 let mut on_update_runtime: Option<Expr> = None;
16888 let mut generated_stored_expr: Option<Box<Expr>> = None;
16889 let mut identity_always = false;
16890 loop {
16891 // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
16892 // not-null constraints by name and pg_dump emits them
16893 // inline: `id bigint CONSTRAINT contacts_id_not_null1
16894 // NOT NULL`. Accept and discard the name; whatever
16895 // constraint follows is parsed by the arms below.
16896 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16897 // v7.39 (round 308, V29) — a name on an inline
16898 // REFERENCES belongs to the FOREIGN KEY, and the caller
16899 // (`parse_column_def_with_fk`) is what builds it, so
16900 // leave the whole clause for it. Dropping the name here
16901 // is what made `CONSTRAINT fk_a REFERENCES …` come back
16902 // as the synthesised `c_pid_fkey` — which then could
16903 // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
16904 // `advance()` takes tokens by `mem::replace`, so there
16905 // is no rewinding once consumed.
16906 if matches!(
16907 self.tokens.get(self.pos + 2),
16908 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
16909 ) {
16910 break;
16911 }
16912 self.advance();
16913 let _name = self.expect_ident_like()?;
16914 continue;
16915 }
16916 // v7.39 (round 379) — MySQL's SHORT generated-column form
16917 // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
16918 // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
16919 // below), but hand-written schemas and app migrations use this.
16920 // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
16921 // SPG computes-and-stores either way, like the long form.
16922 if matches!(self.peek(), Token::As) {
16923 self.advance();
16924 if !matches!(self.peek(), Token::LParen) {
16925 return Err(self.err(alloc::format!(
16926 "expected '(' after AS in a generated column, got {:?}",
16927 self.peek()
16928 )));
16929 }
16930 self.advance();
16931 let expr = self.parse_expr(0)?;
16932 if !matches!(self.peek(), Token::RParen) {
16933 return Err(self.err(alloc::format!(
16934 "expected ')' after AS (<expr>), got {:?}",
16935 self.peek()
16936 )));
16937 }
16938 self.advance();
16939 if matches!(self.peek(), Token::Ident(s)
16940 if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
16941 {
16942 self.advance();
16943 }
16944 generated_stored_expr = Some(alloc::boxed::Box::new(expr));
16945 continue;
16946 }
16947 // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
16948 // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
16949 // the modern replacement for SERIAL in hand-written
16950 // schemas). Both flavours map onto the auto-increment
16951 // machinery — SPG's serial semantics ≈ BY DEFAULT;
16952 // ALWAYS's reject-explicit-values nuance is documented
16953 // leniency. Generated EXPRESSION columns
16954 // (`AS (expr) STORED`) are not supported: error loudly
16955 // instead of silently storing NULLs.
16956 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
16957 self.advance();
16958 let mut saw_generated_always = false;
16959 match self.peek().clone() {
16960 Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
16961 self.advance();
16962 saw_generated_always = true;
16963 }
16964 Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
16965 self.advance();
16966 if !matches!(self.peek(), Token::Default) {
16967 return Err(self.err(alloc::format!(
16968 "expected DEFAULT after GENERATED BY, got {:?}",
16969 self.peek()
16970 )));
16971 }
16972 self.advance();
16973 }
16974 other => {
16975 return Err(self.err(alloc::format!(
16976 "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
16977 )));
16978 }
16979 }
16980 if !matches!(self.peek(), Token::As) {
16981 return Err(self.err(alloc::format!(
16982 "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
16983 self.peek()
16984 )));
16985 }
16986 self.advance();
16987 // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
16988 // ( <expr> ) STORED` stored computed-column. The
16989 // expression is captured for the engine to recompute
16990 // on every INSERT / UPDATE. v7.37.7 accepts the
16991 // STORED keyword only; PG also has VIRTUAL, which
16992 // v7.37.7 carves out (sentori only uses STORED).
16993 if matches!(self.peek(), Token::LParen) {
16994 self.advance();
16995 let expr = self.parse_expr(0)?;
16996 if !matches!(self.peek(), Token::RParen) {
16997 return Err(self.err(alloc::format!(
16998 "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
16999 self.peek()
17000 )));
17001 }
17002 self.advance();
17003 let stored = match self.peek() {
17004 Token::Ident(s) | Token::QuotedIdent(s)
17005 if s.eq_ignore_ascii_case("stored") =>
17006 {
17007 self.advance();
17008 true
17009 }
17010 // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17011 // generated columns. SPG computes them on write and
17012 // persists like STORED; the two are observably
17013 // identical for query results (the value, recompute
17014 // on base-column change, and NOT NULL enforcement all
17015 // match), so a PG 18 schema/dump using VIRTUAL loads
17016 // and behaves correctly. The compute-on-read storage
17017 // saving is an invisible internal difference.
17018 Token::Ident(s) | Token::QuotedIdent(s)
17019 if s.eq_ignore_ascii_case("virtual") =>
17020 {
17021 self.advance();
17022 false
17023 }
17024 other => {
17025 return Err(self.err(alloc::format!(
17026 "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17027 got {other:?}"
17028 )));
17029 }
17030 };
17031 let _ = stored; // STORED / VIRTUAL both compute-and-store.
17032 generated_stored_expr = Some(Box::new(expr));
17033 continue;
17034 }
17035 self.expect_keyword_ident("identity")?;
17036 // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17037 // consume the balanced parens and discard (SPG's
17038 // auto-increment is max+1-scan based).
17039 if matches!(self.peek(), Token::LParen) {
17040 let mut depth = 0usize;
17041 loop {
17042 match self.advance() {
17043 Token::LParen => depth += 1,
17044 Token::RParen => {
17045 depth -= 1;
17046 if depth == 0 {
17047 break;
17048 }
17049 }
17050 Token::Eof => {
17051 return Err(self.err(
17052 "unterminated sequence-options parens after IDENTITY".into(),
17053 ));
17054 }
17055 _ => {}
17056 }
17057 }
17058 }
17059 auto_increment = true;
17060 // v7.38 (read01) — remember the ALWAYS flavour so the engine
17061 // can reject explicit non-DEFAULT INSERT values (unless
17062 // OVERRIDING SYSTEM VALUE) the way PG does.
17063 identity_always = saw_generated_always;
17064 // PG identity columns are implicitly NOT NULL.
17065 nullable = false;
17066 continue;
17067 }
17068 // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17069 // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17070 // is accepted today. The "ON" token is an Ident
17071 // (not reserved) — peek before consuming.
17072 if matches!(self.peek(), Token::On)
17073 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17074 {
17075 self.advance(); // ON
17076 self.advance(); // update
17077 // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17078 let next = self.peek().clone();
17079 match next {
17080 Token::Ident(s) | Token::QuotedIdent(s)
17081 if s.eq_ignore_ascii_case("current_timestamp") =>
17082 {
17083 self.advance();
17084 // Optional `(N)` precision.
17085 if matches!(self.peek(), Token::LParen) {
17086 self.advance();
17087 if !matches!(self.peek(), Token::Integer(_)) {
17088 return Err(self.err(alloc::format!(
17089 "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17090 self.peek()
17091 )));
17092 }
17093 self.advance();
17094 if !matches!(self.peek(), Token::RParen) {
17095 return Err(self.err(alloc::format!(
17096 "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17097 self.peek()
17098 )));
17099 }
17100 self.advance();
17101 }
17102 on_update_runtime = Some(Expr::FunctionCall {
17103 name: "now".into(),
17104 args: Vec::new(),
17105 });
17106 continue;
17107 }
17108 other => {
17109 return Err(self.err(alloc::format!(
17110 "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17111 )));
17112 }
17113 }
17114 }
17115 if matches!(self.peek(), Token::Default) {
17116 if default.is_some() {
17117 return Err(self.err("DEFAULT specified twice".into()));
17118 }
17119 self.advance();
17120 default = Some(self.parse_expr(0)?);
17121 continue;
17122 }
17123 // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17124 // token with NOT NULL and sits EARLIER in the loop than the
17125 // deferrability arm, so without the lookahead it was reported as
17126 // "NOT NULL specified twice" (or "expected NULL after NOT").
17127 if matches!(self.peek(), Token::Not)
17128 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17129 {
17130 // NOT DEFERRABLE — explicit immediate; nothing to carry.
17131 self.consume_optional_deferrable_clauses()?;
17132 continue;
17133 }
17134 if matches!(self.peek(), Token::Not) {
17135 if nullability_seen {
17136 return Err(self.err("NOT NULL specified twice".into()));
17137 }
17138 self.advance();
17139 if !matches!(self.peek(), Token::Null) {
17140 return Err(self.err(format!(
17141 "expected NULL after NOT in column def, got {:?}",
17142 self.peek()
17143 )));
17144 }
17145 self.advance();
17146 nullable = false;
17147 nullability_seen = true;
17148 continue;
17149 }
17150 // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17151 // "this column is nullable" marker (the default in
17152 // standard SQL anyway). mysqldump emits it routinely
17153 // (`col TYPE NULL DEFAULT NULL` for nullable
17154 // timestamps etc). Accept + no-op.
17155 if matches!(self.peek(), Token::Null) {
17156 if nullability_seen && !nullable {
17157 // v7.39 (round 761, F31 tranche 2 #31) — PG's
17158 // sentence, PG18-measured (the table name is the
17159 // caller's; the column half is exact).
17160 return Err(self.err(alloc::format!(
17161 "conflicting NULL/NOT NULL declarations for column \"{name}\""
17162 )));
17163 }
17164 self.advance();
17165 nullable = true;
17166 nullability_seen = true;
17167 continue;
17168 }
17169 // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17170 // arrives as a bare Ident. Match either, case-insensitive.
17171 if let Token::Ident(s) = self.peek()
17172 && (s.eq_ignore_ascii_case("auto_increment")
17173 || s.eq_ignore_ascii_case("autoincrement"))
17174 {
17175 if auto_increment {
17176 return Err(self.err("AUTO_INCREMENT specified twice".into()));
17177 }
17178 self.advance();
17179 auto_increment = true;
17180 continue;
17181 }
17182 // v7.9.13 — inline `PRIMARY KEY` column constraint
17183 // (mailrs F1). Implies `NOT NULL`. The engine creates
17184 // a BTree index for the PK column at CREATE TABLE time
17185 // so FK parent-side index lookups resolve.
17186 // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17187 // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17188 // spelling was a parse error, so a pg_dump carrying one stopped
17189 // mid-restore. The clauses are consumed by the same helper the FK
17190 // path has used since round 288 and recorded nowhere: SPG enforces
17191 // the constraint IMMEDIATELY either way, which fails earlier than
17192 // PG inside a transaction that violates-then-repairs — a refusal,
17193 // not a wrong answer. True deferral is the open remainder of F08.
17194 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17195 || (matches!(self.peek(), Token::Not)
17196 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17197 {
17198 // v7.39 (round 711) — CARRIED now (the storing half of
17199 // F08); round 621 only consumed.
17200 let (d, idef) = self.consume_deferrable_clauses_timed()?;
17201 constraint_deferrable |= d;
17202 constraint_initially_deferred |= idef;
17203 continue;
17204 }
17205 if let Token::Ident(s) = self.peek()
17206 && s.eq_ignore_ascii_case("primary")
17207 {
17208 if is_primary_key {
17209 return Err(self.err("PRIMARY KEY specified twice".into()));
17210 }
17211 // Peek-ahead for the required `KEY` token.
17212 let next = self.tokens.get(self.pos + 1);
17213 let next_is_key = matches!(
17214 next,
17215 Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17216 );
17217 if !next_is_key {
17218 return Err(self.err(format!(
17219 "expected KEY after PRIMARY in column def, got {:?}",
17220 next
17221 )));
17222 }
17223 self.advance(); // PRIMARY
17224 self.advance(); // KEY
17225 is_primary_key = true;
17226 if nullability_seen && nullable {
17227 return Err(self.err(
17228 "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17229 ));
17230 }
17231 nullable = false;
17232 nullability_seen = true;
17233 continue;
17234 }
17235 // v7.13.0 — inline `UNIQUE` column constraint
17236 // (mailrs round-5 G2). Fold into a single-column
17237 // table-level UNIQUE at CREATE TABLE post-process time.
17238 if let Token::Ident(s) = self.peek()
17239 && s.eq_ignore_ascii_case("unique")
17240 {
17241 if is_unique {
17242 return Err(self.err("UNIQUE specified twice".into()));
17243 }
17244 self.advance();
17245 is_unique = true;
17246 // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17247 // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17248 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17249 let n1 = self.tokens.get(self.pos + 1);
17250 let n2 = self.tokens.get(self.pos + 2);
17251 if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17252 self.advance(); // NULLS
17253 self.advance(); // NOT
17254 self.advance(); // DISTINCT
17255 unique_nulls_not_distinct = true;
17256 } else if matches!(n1, Some(Token::Distinct)) {
17257 self.advance(); // NULLS
17258 self.advance(); // DISTINCT
17259 }
17260 }
17261 continue;
17262 }
17263 // v7.13.0 — inline `CHECK (<expr>)` column constraint
17264 // (mailrs round-5 G3). PG semantics: column-level
17265 // CHECK is equivalent to a table-level CHECK. Multiple
17266 // inline CHECKs on the same column AND together.
17267 if let Token::Ident(s) = self.peek()
17268 && s.eq_ignore_ascii_case("check")
17269 {
17270 self.advance();
17271 if !matches!(self.peek(), Token::LParen) {
17272 return Err(self.err(alloc::format!(
17273 "expected '(' after CHECK in column def, got {:?}",
17274 self.peek()
17275 )));
17276 }
17277 self.advance();
17278 let pred = self.parse_expr(0)?;
17279 if !matches!(self.peek(), Token::RParen) {
17280 return Err(self.err(alloc::format!(
17281 "expected ')' to close CHECK predicate, got {:?}",
17282 self.peek()
17283 )));
17284 }
17285 self.advance();
17286 check = Some(match check.take() {
17287 Some(prev) => Expr::Binary {
17288 op: BinOp::And,
17289 lhs: Box::new(prev),
17290 rhs: Box::new(pred),
17291 },
17292 None => pred,
17293 });
17294 continue;
17295 }
17296 break;
17297 }
17298 Ok(ColumnDef {
17299 name,
17300 ty,
17301 nullable,
17302 default,
17303 auto_increment,
17304 is_primary_key,
17305 is_unique,
17306 unique_nulls_not_distinct,
17307 constraint_deferrable,
17308 constraint_initially_deferred,
17309 check,
17310 user_type_ref,
17311 on_update_runtime,
17312 collation,
17313 collation_explicit,
17314 collation_name,
17315 is_unsigned,
17316 inline_enum_variants,
17317 inline_set_variants,
17318 generated_stored_expr,
17319 identity_always,
17320 mysql_int_width,
17321 mysql_fsp,
17322 })
17323 }
17324
17325 /// `NUMERIC` may appear without parameters, with one (precision
17326 /// only, scale=0), or with both. Returns `(precision, scale)` with
17327 /// 0 = unspecified for the bare form.
17328 fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17329 if !matches!(self.peek(), Token::LParen) {
17330 // Bare `NUMERIC` — PG treats this as "unlimited precision";
17331 // we surface it as precision=0 to mean "unconstrained" so
17332 // the engine doesn't need a separate variant.
17333 return Ok((0, 0));
17334 }
17335 self.advance();
17336 // v7.39 (round 272) — PG's declared precision runs to 1000, and
17337 // it words the out-of-range case with the value it saw. SPG
17338 // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17339 // accepts failed to parse at all; values wider than i128 are
17340 // carried by the arbitrary-precision form.
17341 let precision = match self.advance() {
17342 Token::Integer(n) if (1..=1000).contains(&n) => {
17343 u16::try_from(n).expect("range-checked")
17344 }
17345 Token::Integer(n) => {
17346 return Err(ParseError {
17347 message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17348 token_pos: self.consumed_pos(),
17349 });
17350 }
17351 other => {
17352 return Err(ParseError {
17353 message: format!(
17354 "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17355 ),
17356 token_pos: self.consumed_pos(),
17357 });
17358 }
17359 };
17360 // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17361 // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17362 // then overflows). A negative scale rounds to tens / hundreds / …
17363 let scale = if matches!(self.peek(), Token::Comma) {
17364 self.advance();
17365 let neg = if matches!(self.peek(), Token::Minus) {
17366 self.advance();
17367 true
17368 } else {
17369 false
17370 };
17371 match self.advance() {
17372 Token::Integer(n) => {
17373 let signed = if neg { -n } else { n };
17374 if !(-1000..=1000).contains(&signed) {
17375 return Err(ParseError {
17376 message: format!(
17377 "NUMERIC scale {signed} must be between -1000 and 1000"
17378 ),
17379 token_pos: self.consumed_pos(),
17380 });
17381 }
17382 i16::try_from(signed).expect("range-checked")
17383 }
17384 other => {
17385 return Err(ParseError {
17386 message: format!("NUMERIC scale must be an integer, got {other:?}"),
17387 token_pos: self.consumed_pos(),
17388 });
17389 }
17390 }
17391 } else {
17392 0
17393 };
17394 if !matches!(self.peek(), Token::RParen) {
17395 return Err(self.err(format!(
17396 "expected ')' to close NUMERIC params, got {:?}",
17397 self.peek()
17398 )));
17399 }
17400 self.advance();
17401 Ok((precision, scale))
17402 }
17403
17404 /// Parse `(N)` where `N` is a positive integer literal — used by the
17405 /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
17406 /// for the error message.
17407 /// v6.0.1: parse the optional `USING <encoding>` clause that
17408 /// follows `VECTOR(N)` in a column definition. Missing clause
17409 /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
17410 /// ident → `ParseError` listing the encodings recognised today.
17411 fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
17412 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
17413 return Ok(VecEncoding::F32);
17414 }
17415 // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
17416 // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
17417 // consume the token when the very next token is a known
17418 // vector-encoding keyword (SQ8 / HALF). Otherwise leave
17419 // `USING` for the caller — it's the rewrite-expression form.
17420 let n1 = self.tokens.get(self.pos + 1);
17421 let next_is_encoding = matches!(
17422 n1,
17423 Some(Token::Ident(s))
17424 if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
17425 );
17426 if !next_is_encoding {
17427 return Ok(VecEncoding::F32);
17428 }
17429 self.advance();
17430 let enc_ident = match self.advance() {
17431 Token::Ident(s) => s,
17432 other => {
17433 return Err(self.err(format!(
17434 "expected vector encoding after USING, got {other:?}"
17435 )));
17436 }
17437 };
17438 match enc_ident.to_ascii_lowercase().as_str() {
17439 "sq8" => Ok(VecEncoding::Sq8),
17440 // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
17441 // binary16 per-element storage.
17442 "half" => Ok(VecEncoding::F16),
17443 other => Err(self.err(format!(
17444 "unknown vector encoding {other:?}; supported: SQ8, HALF"
17445 ))),
17446 }
17447 }
17448
17449 /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
17450 /// without consuming it. Returns `Some(N)` when the next
17451 /// tokens are `( <int> )`; None otherwise. Used by the
17452 /// TINYINT classifier to decide whether to map to Bool or
17453 /// SmallInt.
17454 fn peek_optional_paren_size_value(&self) -> Option<i64> {
17455 if !matches!(self.peek(), Token::LParen) {
17456 return None;
17457 }
17458 let next = self.tokens.get(self.pos + 1)?;
17459 let n = match next {
17460 Token::Integer(n) => *n,
17461 _ => return None,
17462 };
17463 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17464 return None;
17465 }
17466 Some(n)
17467 }
17468
17469 /// v7.14.0 — consume an optional MySQL display-width
17470 /// parenthesised number after an integer type, returning
17471 /// nothing. `TINYINT(1)` etc.
17472 /// v7.39 (round 360) — does the parenthesised group ahead contain a
17473 /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
17474 fn peek_paren_has_comma(&self) -> bool {
17475 let mut i = self.pos + 1;
17476 let mut depth = 1usize;
17477 while depth > 0 {
17478 match self.tokens.get(i) {
17479 Some(Token::LParen) => depth += 1,
17480 Some(Token::RParen) => depth -= 1,
17481 Some(Token::Comma) if depth == 1 => return true,
17482 None | Some(Token::Eof) => return false,
17483 _ => {}
17484 }
17485 i += 1;
17486 }
17487 false
17488 }
17489
17490 /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
17491 /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
17492 /// fractional-seconds precision that drives write truncation and render
17493 /// padding, where `consume_optional_paren_size` throws it away.
17494 /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
17495 fn take_optional_paren_size(&mut self) -> Option<u8> {
17496 let Some(Token::Integer(n)) = self
17497 .tokens
17498 .get(self.pos + 1)
17499 .filter(|_| matches!(self.peek(), Token::LParen))
17500 .cloned()
17501 else {
17502 self.consume_optional_paren_size();
17503 return None;
17504 };
17505 if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
17506 self.consume_optional_paren_size();
17507 return None;
17508 }
17509 self.consume_optional_paren_size();
17510 u8::try_from(n).ok()
17511 }
17512
17513 fn consume_optional_paren_size(&mut self) {
17514 if !matches!(self.peek(), Token::LParen) {
17515 return;
17516 }
17517 self.advance();
17518 // Skip until matching RParen (allow nested or any tokens).
17519 let mut depth = 1usize;
17520 while depth > 0 {
17521 match self.peek() {
17522 Token::LParen => depth += 1,
17523 Token::RParen => depth -= 1,
17524 Token::Eof => return,
17525 _ => {}
17526 }
17527 self.advance();
17528 }
17529 }
17530
17531 fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
17532 if !matches!(self.peek(), Token::LParen) {
17533 return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
17534 }
17535 self.advance();
17536 let n = match self.advance() {
17537 Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
17538 message: format!("{label} size too large: {n}"),
17539 token_pos: self.consumed_pos(),
17540 })?,
17541 other => {
17542 return Err(ParseError {
17543 message: format!("expected positive integer {label} size, got {other:?}"),
17544 token_pos: self.consumed_pos(),
17545 });
17546 }
17547 };
17548 if !matches!(self.peek(), Token::RParen) {
17549 return Err(self.err(format!(
17550 "expected ')' after {label} size, got {:?}",
17551 self.peek()
17552 )));
17553 }
17554 self.advance();
17555 Ok(n)
17556 }
17557
17558 /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
17559 /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
17560 /// key, like MySQL) whose action skips conflicting rows.
17561 /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
17562 /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
17563 /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
17564 /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
17565 /// common bulk-upsert spellings —
17566 /// INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
17567 /// REPLACE INTO t SELECT …
17568 /// — were a parse error / a duplicate-key failure respectively.
17569 ///
17570 /// Precedence: an explicitly written clause beats a statement-level flag.
17571 /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
17572 /// implicit `REPLACE` and `IGNORE` lowerings.
17573 fn parse_insert_conflict_clause(
17574 &mut self,
17575 replace: bool,
17576 ignore: bool,
17577 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17578 if let Some(c) = self.parse_optional_on_duplicate_key()? {
17579 return Ok(Some(c));
17580 }
17581 if let Some(c) = self.parse_optional_on_conflict()? {
17582 return Ok(Some(c));
17583 }
17584 if replace {
17585 // REPLACE INTO = delete-then-insert, which PG spells as
17586 // `ON CONFLICT DO UPDATE SET` over every column; the engine
17587 // reads an empty assignment list as "take the incoming row".
17588 return Ok(Some(crate::ast::OnConflictClause {
17589 target_columns: Vec::new(),
17590 index_where: None,
17591 constraint_name: None,
17592 mysql_lowered: true,
17593 action: crate::ast::OnConflictAction::Update {
17594 assignments: Vec::new(),
17595 where_: None,
17596 },
17597 }));
17598 }
17599 if ignore {
17600 return Ok(Some(Self::insert_ignore_clause()));
17601 }
17602 Ok(None)
17603 }
17604
17605 /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
17606 /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
17607 /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
17608 /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
17609 fn parse_optional_on_duplicate_key(
17610 &mut self,
17611 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
17612 if !(matches!(self.peek(), Token::On)
17613 && matches!(self.tokens.get(self.pos + 1),
17614 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
17615 {
17616 return Ok(None);
17617 }
17618 self.advance(); // ON
17619 self.advance(); // DUPLICATE
17620 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
17621 return Err(self.err(format!(
17622 "expected KEY after ON DUPLICATE, got {:?}",
17623 self.peek()
17624 )));
17625 }
17626 self.advance();
17627 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
17628 return Err(self.err(format!(
17629 "expected UPDATE after ON DUPLICATE KEY, got {:?}",
17630 self.peek()
17631 )));
17632 }
17633 self.advance();
17634 let mut assignments: Vec<(String, Expr)> = Vec::new();
17635 loop {
17636 let col = self.expect_ident_like()?;
17637 if !matches!(self.peek(), Token::Eq) {
17638 return Err(self.err(format!(
17639 "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
17640 self.peek()
17641 )));
17642 }
17643 self.advance();
17644 let mut expr = self.parse_expr(0)?;
17645 Self::rewrite_mysql_values_refs(&mut expr);
17646 assignments.push((col, expr));
17647 if matches!(self.peek(), Token::Comma) {
17648 self.advance();
17649 continue;
17650 }
17651 break;
17652 }
17653 Ok(Some(crate::ast::OnConflictClause {
17654 target_columns: Vec::new(),
17655 index_where: None,
17656 constraint_name: None,
17657 mysql_lowered: true,
17658 action: crate::ast::OnConflictAction::Update {
17659 assignments,
17660 where_: None,
17661 },
17662 }))
17663 }
17664
17665 fn insert_ignore_clause() -> crate::ast::OnConflictClause {
17666 crate::ast::OnConflictClause {
17667 target_columns: Vec::new(),
17668 index_where: None,
17669 constraint_name: None,
17670 mysql_lowered: true,
17671 action: crate::ast::OnConflictAction::Nothing,
17672 }
17673 }
17674
17675 fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
17676 debug_assert!(
17677 matches!(self.peek(), Token::Insert)
17678 || (replace
17679 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
17680 );
17681 self.advance();
17682 // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
17683 // would raise a duplicate-key error instead of failing the statement,
17684 // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
17685 // plain ident to the lexer; only the MySQL dialect accepts it here.
17686 let ignore = self.mysql_dialect
17687 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
17688 if ignore {
17689 self.advance();
17690 }
17691 if !matches!(self.peek(), Token::Into) {
17692 return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
17693 }
17694 self.advance();
17695 let table = self.expect_ident_like()?;
17696 // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
17697 // grammar requires the AS keyword here (a bare identifier would be
17698 // ambiguous with a column list). The alias is what the ON CONFLICT
17699 // DO UPDATE expressions refer to the target row by.
17700 let alias = if matches!(self.peek(), Token::As) {
17701 self.advance();
17702 Some(self.expect_ident_like()?)
17703 } else {
17704 None
17705 };
17706 // v7.39 (round 428) — MySQL's SET-form INSERT:
17707 // INSERT INTO t SET a = 1, b = 'x'
17708 // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
17709 // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
17710 // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
17711 // measured). So it lowers to the column list + one VALUES row and
17712 // rejoins the ordinary path, which already handles every one of
17713 // those. PG has no such spelling, hence the dialect gate.
17714 if self.mysql_dialect
17715 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
17716 {
17717 self.advance(); // SET
17718 let mut names = Vec::new();
17719 let mut values = Vec::new();
17720 loop {
17721 names.push(self.expect_ident_like()?);
17722 if !matches!(self.peek(), Token::Eq) {
17723 return Err(self.err(alloc::format!(
17724 "expected '=' in INSERT … SET, got {:?}",
17725 self.peek()
17726 )));
17727 }
17728 self.advance();
17729 // `SET a = DEFAULT` rides the same `__column_default` marker
17730 // the VALUES-row and UPDATE-SET paths use; the INSERT
17731 // executor resolves it against the target column.
17732 if matches!(self.peek(), Token::Default) {
17733 self.advance();
17734 values.push(Expr::FunctionCall {
17735 name: "__column_default".to_string(),
17736 args: Vec::new(),
17737 });
17738 } else {
17739 values.push(self.parse_expr(0)?);
17740 }
17741 if matches!(self.peek(), Token::Comma) {
17742 self.advance();
17743 continue;
17744 }
17745 break;
17746 }
17747 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17748 let returning = self.parse_optional_returning()?;
17749 return Ok(Statement::Insert(InsertStatement {
17750 ctes: Vec::new(),
17751 table,
17752 alias,
17753 columns: Some(names),
17754 rows: alloc::vec![values],
17755 select_source: None,
17756 // MySQL's SET form has no `OVERRIDING …` clause (that is
17757 // PG's identity-column spelling).
17758 overriding: Overriding::None,
17759 mysql_ignore: ignore,
17760 on_conflict,
17761 returning,
17762 }));
17763 }
17764 // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
17765 // v7.39 (round 151) — a SELECT or WITH right after the paren is
17766 // a parenthesized query source instead (PG select_with_parens:
17767 // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
17768 // both keywords are reserved in PG, so no column list can start
17769 // with them.
17770 let columns = if matches!(self.peek(), Token::LParen) {
17771 self.advance();
17772 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17773 let select_stmt = if self.peek_is_with_kw() {
17774 self.advance();
17775 self.parse_nested_with_select()?
17776 } else {
17777 match self.parse_select_stmt()? {
17778 Statement::Select(s) => s,
17779 other => {
17780 return Err(self.err(alloc::format!(
17781 "expected SELECT in parenthesized INSERT source, got {other:?}"
17782 )));
17783 }
17784 }
17785 };
17786 if !matches!(self.peek(), Token::RParen) {
17787 return Err(self.err(format!(
17788 "expected ')' after parenthesized INSERT source, got {:?}",
17789 self.peek()
17790 )));
17791 }
17792 self.advance();
17793 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17794 let returning = self.parse_optional_returning()?;
17795 return Ok(Statement::Insert(InsertStatement {
17796 ctes: Vec::new(),
17797 table,
17798 alias: alias.clone(),
17799 columns: None,
17800 rows: Vec::new(),
17801 select_source: Some(Box::new(select_stmt)),
17802 on_conflict,
17803 returning,
17804 overriding: Overriding::None,
17805 mysql_ignore: ignore,
17806 }));
17807 }
17808 let mut names = Vec::new();
17809 loop {
17810 names.push(self.expect_ident_like()?);
17811 match self.peek() {
17812 Token::Comma => {
17813 self.advance();
17814 }
17815 Token::RParen => {
17816 self.advance();
17817 break;
17818 }
17819 other => {
17820 return Err(self.err(format!(
17821 "expected ',' or ')' in INSERT column list, got {other:?}"
17822 )));
17823 }
17824 }
17825 }
17826 Some(names)
17827 } else {
17828 None
17829 };
17830 // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
17831 // OVERRIDING SYSTEM VALUE for its identity columns. The clause
17832 // is captured on the statement so the engine can apply PG's
17833 // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
17834 let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
17835 {
17836 self.advance();
17837 let which = self.expect_ident_like()?;
17838 let ov = if which.eq_ignore_ascii_case("system") {
17839 Overriding::System
17840 } else if which.eq_ignore_ascii_case("user") {
17841 Overriding::User
17842 } else {
17843 return Err(self.err(format!(
17844 "expected SYSTEM or USER after OVERRIDING, got {which:?}"
17845 )));
17846 };
17847 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
17848 return Err(self.err(format!(
17849 "expected VALUE after OVERRIDING {}, got {:?}",
17850 which.to_ascii_uppercase(),
17851 self.peek()
17852 )));
17853 }
17854 self.advance();
17855 ov
17856 } else {
17857 Overriding::None
17858 };
17859 // `INSERT INTO t DEFAULT VALUES` — a single row made
17860 // entirely of column defaults. Lower to the permuted
17861 // column-list path with an empty list: every schema column
17862 // is unmapped, so the engine fills each from its default
17863 // (serials advance, plain defaults evaluate, the rest NULL).
17864 if matches!(self.peek(), Token::Default) {
17865 self.advance();
17866 if !matches!(self.peek(), Token::Values) {
17867 return Err(self.err(format!(
17868 "expected VALUES after DEFAULT in INSERT, got {:?}",
17869 self.peek()
17870 )));
17871 }
17872 self.advance();
17873 if columns.is_some() {
17874 return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
17875 }
17876 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17877 let returning = self.parse_optional_returning()?;
17878 return Ok(Statement::Insert(InsertStatement {
17879 ctes: Vec::new(),
17880 table,
17881 alias: alias.clone(),
17882 columns: Some(Vec::new()),
17883 rows: alloc::vec![Vec::new()],
17884 select_source: None,
17885 on_conflict,
17886 returning,
17887 overriding,
17888 mysql_ignore: ignore,
17889 }));
17890 }
17891 // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
17892 // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
17893 // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
17894 // SELECT …`) heads the SOURCE select, as in PG (the statement's
17895 // own WITH comes before INSERT).
17896 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
17897 let select_stmt = if self.peek_is_with_kw() {
17898 self.advance();
17899 self.parse_nested_with_select()?
17900 } else {
17901 match self.parse_select_stmt()? {
17902 Statement::Select(s) => s,
17903 other => {
17904 return Err(self.err(alloc::format!(
17905 "expected SELECT after INSERT INTO ... target, got {other:?}"
17906 )));
17907 }
17908 }
17909 };
17910 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17911 let returning = self.parse_optional_returning()?;
17912 return Ok(Statement::Insert(InsertStatement {
17913 ctes: Vec::new(),
17914 table,
17915 alias: alias.clone(),
17916 columns,
17917 rows: Vec::new(),
17918 select_source: Some(Box::new(select_stmt)),
17919 on_conflict,
17920 returning,
17921 overriding,
17922 mysql_ignore: ignore,
17923 }));
17924 }
17925 if !matches!(self.peek(), Token::Values) {
17926 return Err(self.err(format!(
17927 "expected VALUES or SELECT after table name, got {:?}",
17928 self.peek()
17929 )));
17930 }
17931 self.advance();
17932 if !matches!(self.peek(), Token::LParen) {
17933 return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
17934 }
17935 let mut rows = Vec::new();
17936 loop {
17937 // Each iteration consumes one `(expr, expr, …)` tuple.
17938 if !matches!(self.peek(), Token::LParen) {
17939 return Err(self.err(format!(
17940 "expected '(' for next VALUES tuple, got {:?}",
17941 self.peek()
17942 )));
17943 }
17944 self.advance();
17945 let mut tuple = Vec::new();
17946 loop {
17947 // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
17948 // the column's declared default for that slot. Rides out as the
17949 // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
17950 // path uses; the INSERT executor resolves it per target column.
17951 if matches!(self.peek(), Token::Default) {
17952 self.advance();
17953 tuple.push(Expr::FunctionCall {
17954 name: "__column_default".to_string(),
17955 args: Vec::new(),
17956 });
17957 } else {
17958 tuple.push(self.parse_expr(0)?);
17959 }
17960 match self.peek() {
17961 Token::Comma => {
17962 self.advance();
17963 }
17964 Token::RParen => {
17965 self.advance();
17966 break;
17967 }
17968 other => {
17969 return Err(self.err(format!(
17970 "expected ',' or ')' in VALUES tuple, got {other:?}"
17971 )));
17972 }
17973 }
17974 }
17975 if tuple.is_empty() {
17976 return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
17977 }
17978 rows.push(tuple);
17979 // Continue with comma-separated tuples.
17980 if matches!(self.peek(), Token::Comma) {
17981 self.advance();
17982 } else {
17983 break;
17984 }
17985 }
17986 // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
17987 // to ON CONFLICT DO UPDATE with an empty conflict target
17988 // (the engine picks the table's first unique index, which
17989 // matches MySQL's any-unique-key behaviour for the common
17990 // single-key case). `VALUES(col)` in the assignments is
17991 // MySQL's spelling of EXCLUDED.col.
17992 let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
17993 let returning = self.parse_optional_returning()?;
17994 Ok(Statement::Insert(InsertStatement {
17995 ctes: Vec::new(),
17996 table,
17997 alias,
17998 columns,
17999 rows,
18000 select_source: None,
18001 on_conflict,
18002 returning,
18003 overriding,
18004 mysql_ignore: ignore,
18005 }))
18006 }
18007
18008 /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18009 /// the incoming row's value — exactly PG's EXCLUDED.col.
18010 fn rewrite_mysql_values_refs(e: &mut Expr) {
18011 match e {
18012 Expr::FunctionCall { name, args }
18013 if name.eq_ignore_ascii_case("values")
18014 && args.len() == 1
18015 && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18016 {
18017 let Expr::Column(c) = &args[0] else {
18018 unreachable!("guarded above");
18019 };
18020 *e = Expr::Column(crate::ast::ColumnName {
18021 qualifier: Some("EXCLUDED".to_string()),
18022 name: c.name.clone(),
18023 });
18024 }
18025 Expr::FunctionCall { args, .. } => {
18026 for a in args {
18027 Self::rewrite_mysql_values_refs(a);
18028 }
18029 }
18030 Expr::Binary { lhs, rhs, .. } => {
18031 Self::rewrite_mysql_values_refs(lhs);
18032 Self::rewrite_mysql_values_refs(rhs);
18033 }
18034 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18035 Self::rewrite_mysql_values_refs(expr);
18036 }
18037 Expr::Case {
18038 operand,
18039 branches,
18040 else_branch,
18041 } => {
18042 if let Some(op) = operand {
18043 Self::rewrite_mysql_values_refs(op);
18044 }
18045 for (w, t) in branches {
18046 Self::rewrite_mysql_values_refs(w);
18047 Self::rewrite_mysql_values_refs(t);
18048 }
18049 if let Some(el) = else_branch {
18050 Self::rewrite_mysql_values_refs(el);
18051 }
18052 }
18053 _ => {}
18054 }
18055 }
18056
18057 /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18058 /// clause sitting between the INSERT body and the trailing
18059 /// RETURNING. All keywords come in as bare idents; `ON` is
18060 /// a reserved Token though.
18061 fn parse_optional_on_conflict(
18062 &mut self,
18063 ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18064 if !matches!(self.peek(), Token::On) {
18065 return Ok(None);
18066 }
18067 // Peek further: we want exactly "ON CONFLICT ...". If the
18068 // next ident isn't "conflict", let some other parser handle.
18069 let next_is_conflict = matches!(
18070 self.tokens.get(self.pos + 1),
18071 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18072 );
18073 if !next_is_conflict {
18074 return Ok(None);
18075 }
18076 self.advance(); // ON
18077 self.advance(); // CONFLICT
18078 // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18079 // the constraint instead of listing columns (the pg_dump
18080 // form); the engine resolves it.
18081 let mut constraint_name: Option<String> = None;
18082 if matches!(self.peek(), Token::On) {
18083 self.advance(); // ON
18084 match self.advance() {
18085 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18086 }
18087 other => {
18088 return Err(self.err(alloc::format!(
18089 "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18090 )));
18091 }
18092 }
18093 constraint_name = Some(self.expect_ident_like()?);
18094 }
18095 // Optional `(col [, col]*)` target list.
18096 let mut target_columns: Vec<String> = Vec::new();
18097 if matches!(self.peek(), Token::LParen) {
18098 self.advance();
18099 loop {
18100 target_columns.push(self.expect_ident_like()?);
18101 match self.peek() {
18102 Token::Comma => {
18103 self.advance();
18104 }
18105 Token::RParen => {
18106 self.advance();
18107 break;
18108 }
18109 other => {
18110 return Err(self.err(alloc::format!(
18111 "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18112 )));
18113 }
18114 }
18115 }
18116 }
18117 // v7.39 (round 240) — optional index predicate after the target
18118 // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18119 // PARTIAL unique index; SPG's arbiters are full indexes, which
18120 // satisfy any predicate, so it is parsed and carried but not
18121 // consulted (recorded residual: partial-unique-index arbiters).
18122 let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18123 self.advance();
18124 Some(self.parse_expr(0)?)
18125 } else {
18126 None
18127 };
18128 // Required `DO`.
18129 match self.advance() {
18130 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18131 other => {
18132 return Err(self.err(alloc::format!(
18133 "expected DO after ON CONFLICT [(…)], got {other:?}"
18134 )));
18135 }
18136 }
18137 // Action: NOTHING | UPDATE SET …
18138 let action = match self.advance() {
18139 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18140 crate::ast::OnConflictAction::Nothing
18141 }
18142 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18143 self.parse_on_conflict_update_action()?
18144 }
18145 other => {
18146 return Err(self.err(alloc::format!(
18147 "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18148 )));
18149 }
18150 };
18151 Ok(Some(crate::ast::OnConflictClause {
18152 target_columns,
18153 index_where,
18154 constraint_name,
18155 mysql_lowered: false,
18156 action,
18157 }))
18158 }
18159
18160 /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18161 /// `SET col = expr [, …] [WHERE cond]`. Caller already
18162 /// consumed `UPDATE`.
18163 fn parse_on_conflict_update_action(
18164 &mut self,
18165 ) -> Result<crate::ast::OnConflictAction, ParseError> {
18166 // `SET`
18167 match self.advance() {
18168 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18169 other => {
18170 return Err(self.err(alloc::format!(
18171 "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18172 )));
18173 }
18174 }
18175 let mut assignments: Vec<(String, Expr)> = Vec::new();
18176 loop {
18177 let col = self.expect_ident_like()?;
18178 if !matches!(self.peek(), Token::Eq) {
18179 return Err(self.err(alloc::format!(
18180 "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18181 self.peek()
18182 )));
18183 }
18184 self.advance();
18185 let value = self.parse_expr(0)?;
18186 assignments.push((col, value));
18187 if matches!(self.peek(), Token::Comma) {
18188 self.advance();
18189 continue;
18190 }
18191 break;
18192 }
18193 let where_ = if matches!(self.peek(), Token::Where) {
18194 self.advance();
18195 Some(self.parse_expr(0)?)
18196 } else {
18197 None
18198 };
18199 Ok(crate::ast::OnConflictAction::Update {
18200 assignments,
18201 where_,
18202 })
18203 }
18204
18205 fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18206 let mut items = Vec::new();
18207 // v7.39 (round 341, V66) — PG's target list may be EMPTY
18208 // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18209 // answers one zero-column row per row of t, and a bare `SELECT`
18210 // answers a single zero-column row. SPG required at least one
18211 // item, so both were syntax errors. Recognised by the token that
18212 // follows — nothing that can start an expression appears here.
18213 if self.select_list_is_empty_here() {
18214 return Ok(items);
18215 }
18216 loop {
18217 items.push(self.parse_select_item()?);
18218 if matches!(self.peek(), Token::Comma) {
18219 self.advance();
18220 } else {
18221 break;
18222 }
18223 }
18224 Ok(items)
18225 }
18226
18227 /// Is the target list empty at this point — i.e. does the next token
18228 /// end the SELECT's item list rather than start an item?
18229 fn select_list_is_empty_here(&self) -> bool {
18230 match self.peek() {
18231 Token::From
18232 | Token::Where
18233 | Token::Group
18234 | Token::Having
18235 | Token::Order
18236 | Token::Limit
18237 | Token::Offset
18238 | Token::Semicolon
18239 | Token::RParen
18240 | Token::Union
18241 | Token::Except
18242 | Token::Eof => true,
18243 // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18244 // with unreserved keywords, so they arrive as plain idents.
18245 Token::Ident(s) => {
18246 s.eq_ignore_ascii_case("fetch")
18247 || s.eq_ignore_ascii_case("window")
18248 || s.eq_ignore_ascii_case("intersect")
18249 }
18250 _ => false,
18251 }
18252 }
18253
18254 fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18255 if matches!(self.peek(), Token::Star) {
18256 self.advance();
18257 return Ok(SelectItem::Wildcard);
18258 }
18259 // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18260 // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18261 // choke on the `*` ("expected identifier, got Star"). The lookahead is
18262 // `<ident> . *` with nothing binding tighter.
18263 if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18264 if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18265 && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18266 {
18267 self.advance(); // qualifier
18268 self.advance(); // .
18269 self.advance(); // *
18270 return Ok(SelectItem::QualifiedWildcard(q));
18271 }
18272 }
18273 let start_tok = self.pos;
18274 let expr = self.parse_expr(0)?;
18275 let end_tok = self.consumed_pos();
18276 // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18277 // multi-column function returns into columns. Marked here and lowered in
18278 // `parse_bare_select`, where the FROM clause is in hand.
18279 if matches!(self.peek(), Token::Dot)
18280 && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18281 {
18282 self.advance(); // .
18283 self.advance(); // *
18284 return Ok(SelectItem::Expr {
18285 expr: Expr::FunctionCall {
18286 name: "__record_expand".to_string(),
18287 args: alloc::vec![expr],
18288 },
18289 alias: None,
18290 });
18291 }
18292 let alias = match self.parse_optional_alias()? {
18293 Some(a) => Some(a),
18294 None => self.mysql_item_label(&expr, start_tok, end_tok),
18295 };
18296 Ok(SelectItem::Expr { expr, alias })
18297 }
18298
18299 /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18300 /// carries no `AS`, filled in here so every downstream path reports it
18301 /// without knowing the rule. `None` leaves the item un-aliased, which is
18302 /// what a PG session always gets.
18303 ///
18304 /// Measured against MariaDB 11, three rules and no more:
18305 ///
18306 /// | item | label | why |
18307 /// |------------------|------------|------------------------------|
18308 /// | `lbl.a` | `a` | a column reports its name |
18309 /// | `'it''s'` | `it's` | a string reports its VALUE |
18310 /// | `a + b` | `a + b` | anything else, source text |
18311 ///
18312 /// The third is why this lives in the parser at all: the label is the
18313 /// text the client WROTE, down to the spacing, so it cannot be printed
18314 /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18315 ///
18316 /// Comments survive, and that is right: through a `mariadb` CLI both
18317 /// servers answer `a + b` for `SELECT a /* c */ + b`, but that is the
18318 /// CLIENT stripping the comment before it sends. Asked over the raw
18319 /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18320 /// produces.
18321 fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18322 if !self.mysql_dialect {
18323 return None;
18324 }
18325 match expr {
18326 // A column already reports its own name downstream; naming it
18327 // again here would only re-state the qualifier the label drops.
18328 Expr::Column(_) => None,
18329 Expr::Literal(Literal::String(v)) => Some(v.clone()),
18330 _ => self.source_span(start_tok, end_tok).map(str::to_string),
18331 }
18332 }
18333
18334 /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18335 /// consumed VALUES keyword. Each row lowers to a constant SELECT
18336 /// with PG's default column1..columnN names; subsequent rows
18337 /// chain as UNION ALL peers. Shared by the FROM-position
18338 /// `( VALUES … )` arm and the top-level bare VALUES statement.
18339 fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
18340 let mut row_selects: Vec<SelectStatement> = Vec::new();
18341 loop {
18342 if !matches!(self.peek(), Token::LParen) {
18343 return Err(self.err(alloc::format!(
18344 "expected '(' to start a VALUES row, got {:?}",
18345 self.peek()
18346 )));
18347 }
18348 self.advance(); // (
18349 let mut items: Vec<SelectItem> = Vec::new();
18350 loop {
18351 let expr = self.parse_expr(0)?;
18352 items.push(SelectItem::Expr {
18353 expr,
18354 alias: Some(alloc::format!("column{}", items.len() + 1)),
18355 });
18356 match self.peek() {
18357 Token::Comma => {
18358 self.advance();
18359 }
18360 Token::RParen => break,
18361 other => {
18362 return Err(self.err(alloc::format!(
18363 "expected ',' or ')' in VALUES row, got {other:?}"
18364 )));
18365 }
18366 }
18367 }
18368 self.advance(); // )
18369 row_selects.push(SelectStatement {
18370 locking: None,
18371 ctes: Vec::new(),
18372 distinct: false,
18373 distinct_on: Vec::new(),
18374 items,
18375 from: None,
18376 where_: None,
18377 group_by: None,
18378 group_by_all: false,
18379 having: None,
18380 unions: Vec::new(),
18381 order_by: Vec::new(),
18382 limit: None,
18383 offset: None,
18384 limit_with_ties: false,
18385 window_check_exprs: Vec::new(),
18386 });
18387 if matches!(self.peek(), Token::Comma) {
18388 self.advance();
18389 continue;
18390 }
18391 break;
18392 }
18393 let mut head = row_selects.remove(0);
18394 head.unions = row_selects
18395 .into_iter()
18396 .map(|s| (UnionKind::All, s))
18397 .collect();
18398 Ok(head)
18399 }
18400
18401 fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
18402 // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
18403 // children. It was read as a table NAMED `only`, so the query
18404 // failed on `relation "only" does not exist`.
18405 //
18406 // v7.39 (round 644) — and it is no longer a no-op. Round 621
18407 // absorbed the keyword, reasoning that SPG's children are
18408 // separate relations a plain scan does not descend into, so ONLY
18409 // already described the scan. That stopped being true when a
18410 // partition parent started unioning its children: measured,
18411 // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
18412 // where PG answers 0. The flag is carried now.
18413 let mut only = false;
18414 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
18415 && matches!(
18416 self.tokens.get(self.pos + 1),
18417 Some(Token::Ident(_) | Token::QuotedIdent(_))
18418 )
18419 {
18420 only = true;
18421 self.advance();
18422 }
18423 // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
18424 // for these SRFs the keyword is noise at parse time: the
18425 // join executor already substitutes outer-column references
18426 // into unnest_expr / generate_series_args per outer row
18427 // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
18428 // licences the correlation even without the keyword. Absorb
18429 // it and fall through to the SRF arms below.
18430 // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
18431 // just the four builtin SRFs: a user set-returning function on a JOIN's
18432 // right side is the whole point of LATERAL. The keyword stays noise at
18433 // parse time — the join executor substitutes the outer row into the
18434 // call's arguments per outer row.
18435 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18436 && matches!(
18437 self.tokens.get(self.pos + 1),
18438 // The json_each family has its OWN `LATERAL …` arm below, which
18439 // needs to see the keyword — absorbing it here would send those
18440 // calls down the generic table-function channel instead.
18441 Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
18442 )
18443 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18444 {
18445 self.advance(); // LATERAL
18446 }
18447 // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
18448 // set-returning function whose argument may reference a
18449 // preceding FROM item. We rewrite this to
18450 // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
18451 // AS __srf__) AS <alias>` so the existing LATERAL subquery
18452 // executor handles per-outer-row evaluation and the
18453 // SRF-primary jsonb_each_text path handles the inner
18454 // materialisation. Sentori 0067 backfill is the dogfood
18455 // shape.
18456 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18457 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
18458 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18459 {
18460 self.advance(); // LATERAL
18461 let each_fn = match self.peek() {
18462 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18463 _ => unreachable!(),
18464 };
18465 self.advance(); // jsonb_each[_text] / json_each[_text]
18466 self.advance(); // (
18467 let arg = self.parse_expr(0)?;
18468 if !matches!(self.peek(), Token::RParen) {
18469 return Err(self.err(alloc::format!(
18470 "expected ')' after LATERAL {each_fn}() argument, got {:?}",
18471 self.peek()
18472 )));
18473 }
18474 self.advance();
18475 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18476 let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18477 // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
18478 // FROM jsonb_each_text(<arg>) AS __srf__
18479 // PG's `AS kv(key, value)` column-alias list maps
18480 // positions to names; default to (key, value) when
18481 // omitted (matching the SRF's natural column names).
18482 let srf_alias = "__srf__".to_string();
18483 let key_alias = column_aliases
18484 .first()
18485 .cloned()
18486 .unwrap_or_else(|| "key".to_string());
18487 let value_alias = column_aliases
18488 .get(1)
18489 .cloned()
18490 .unwrap_or_else(|| "value".to_string());
18491 let inner_select = crate::ast::SelectStatement {
18492 locking: None,
18493 ctes: Vec::new(),
18494 distinct: false,
18495 distinct_on: Vec::new(),
18496 items: alloc::vec![
18497 crate::ast::SelectItem::Expr {
18498 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18499 qualifier: Some(srf_alias.clone()),
18500 name: "key".to_string(),
18501 }),
18502 alias: Some(key_alias),
18503 },
18504 crate::ast::SelectItem::Expr {
18505 expr: crate::ast::Expr::Column(crate::ast::ColumnName {
18506 qualifier: Some(srf_alias.clone()),
18507 name: "value".to_string(),
18508 }),
18509 alias: Some(value_alias),
18510 },
18511 ],
18512 from: Some(crate::ast::FromClause {
18513 primary: TableRef {
18514 name: srf_alias.clone(),
18515 alias: Some(srf_alias.clone()),
18516 only: false,
18517 as_of_segment: None,
18518 unnest_expr: None,
18519 unnest_column_aliases: Vec::new(),
18520 with_ordinality: false,
18521 generate_series_args: None,
18522 lateral_subquery: None,
18523 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18524 table_fn_call: None,
18525 rows_from: None,
18526 json_table: None,
18527 scalar_fn_item: false,
18528 },
18529 joins: Vec::new(),
18530 }),
18531 where_: None,
18532 group_by: None,
18533 group_by_all: false,
18534 having: None,
18535 unions: Vec::new(),
18536 order_by: Vec::new(),
18537 limit: None,
18538 offset: None,
18539 limit_with_ties: false,
18540 window_check_exprs: Vec::new(),
18541 };
18542 return Ok(TableRef {
18543 name: alias.clone(),
18544 alias: Some(alias),
18545 only: false,
18546 as_of_segment: None,
18547 unnest_expr: None,
18548 unnest_column_aliases: Vec::new(),
18549 with_ordinality: false,
18550 generate_series_args: None,
18551 lateral_subquery: Some(Box::new(inner_select)),
18552 jsonb_each_text_arg: None,
18553 table_fn_call: None,
18554 rows_from: None,
18555 json_table: None,
18556 scalar_fn_item: false,
18557 });
18558 }
18559 // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
18560 // without an explicit `LATERAL` keyword is the same shape
18561 // PG accepts (SRF naturally licences lateral correlation).
18562 // We mirror the LATERAL rewrite when the argument syntactic-
18563 // ally references an outer column (Column { qualifier:
18564 // Some(_), … }). For simplicity we apply the rewrite
18565 // whenever the SRF directly follows JOIN/CROSS JOIN/comma
18566 // in the FROM-list — caller-side join parsing positions
18567 // this peek correctly.
18568 // (Implementation note: detection lives below; the LATERAL
18569 // branch above already covers the explicit form; the bare
18570 // form falls through to the plain SRF arm and the engine
18571 // treats it as a constant-arg SRF if no outer reference is
18572 // present.)
18573 // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
18574 // table. Detect at the head so it claims precedence over
18575 // every other table-ref shape (unnest / generate_series /
18576 // bare ident); the lateral subquery itself follows the
18577 // regular SELECT grammar.
18578 // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
18579 // t(cols)`. Each row lowers to a constant SELECT with PG's
18580 // default column1..columnN names; subsequent rows chain as
18581 // UNION ALL peers. The result rides the derived-table
18582 // lateral_subquery channel — zero executor work.
18583 if matches!(self.peek(), Token::LParen)
18584 && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
18585 {
18586 self.advance(); // (
18587 self.advance(); // VALUES
18588 let head = self.parse_values_rows_body()?;
18589 if !matches!(self.peek(), Token::RParen) {
18590 return Err(self.err(alloc::format!(
18591 "expected ')' after VALUES list, got {:?}",
18592 self.peek()
18593 )));
18594 }
18595 self.advance();
18596 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18597 let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
18598 return Ok(TableRef {
18599 name,
18600 alias: alias_ident,
18601 only: false,
18602 as_of_segment: None,
18603 unnest_expr: None,
18604 unnest_column_aliases: column_aliases,
18605 with_ordinality: false,
18606 generate_series_args: None,
18607 lateral_subquery: Some(Box::new(head)),
18608 jsonb_each_text_arg: None,
18609 table_fn_call: None,
18610 rows_from: None,
18611 json_table: None,
18612 scalar_fn_item: false,
18613 });
18614 }
18615 // v7.37.17 (17.6 siblings) — plain derived table:
18616 // `FROM ( SELECT … ) [AS] alias`. Rides the same
18617 // lateral_subquery channel the explicit LATERAL form uses —
18618 // an uncorrelated inner SELECT executes identically. The
18619 // inner parse carries UNION tails (they live on
18620 // SelectStatement.unions).
18621 // v7.37 D.20 — the derived-table inner may itself be a
18622 // parenthesized set-operation group (`FROM ((SELECT…) UNION
18623 // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
18624 // bare `(SELECT …)`. parse_one_statement already routes a leading
18625 // `(` set-op group (its LParen arm) and a leading WITH
18626 // (parse_with_cte_then_select), so widen the second-token gate to
18627 // Select | LParen | WITH.
18628 // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
18629 // PG's spelling of `SELECT * FROM t` and is accepted wherever a
18630 // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
18631 // has existed since the shorthand landed and `parse_bare_select`
18632 // already routes it ("valid anywhere a SELECT head is"); what was
18633 // missing is this second-token gate, and the CTE body's dispatch
18634 // below. Round 868 found both by putting the shorthand in a
18635 // subquery — the top-level forms had been the only ones tested.
18636 if matches!(self.peek(), Token::LParen)
18637 && (matches!(
18638 self.tokens.get(self.pos + 1),
18639 Some(Token::Select | Token::LParen | Token::Table)
18640 ) || matches!(self.tokens.get(self.pos + 1),
18641 Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
18642 {
18643 self.advance(); // (
18644 let inner = match self.parse_one_statement()? {
18645 Statement::Select(s) => s,
18646 other => {
18647 return Err(self.err(alloc::format!(
18648 "expected SELECT inside derived table ( … ), got {other:?}"
18649 )));
18650 }
18651 };
18652 if !matches!(self.peek(), Token::RParen) {
18653 return Err(self.err(alloc::format!(
18654 "expected ')' after derived-table subquery, got {:?}",
18655 self.peek()
18656 )));
18657 }
18658 self.advance();
18659 // `AS t(a, b)` column-alias list rides the
18660 // unnest_column_aliases field (same positional-rename
18661 // contract the unnest SRFs use).
18662 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18663 let name = alias_ident
18664 .clone()
18665 .unwrap_or_else(|| "subquery".to_string());
18666 return Ok(TableRef {
18667 name,
18668 alias: alias_ident,
18669 only: false,
18670 as_of_segment: None,
18671 unnest_expr: None,
18672 unnest_column_aliases: column_aliases,
18673 with_ordinality: false,
18674 generate_series_args: None,
18675 lateral_subquery: Some(Box::new(inner)),
18676 jsonb_each_text_arg: None,
18677 table_fn_call: None,
18678 rows_from: None,
18679 json_table: None,
18680 scalar_fn_item: false,
18681 });
18682 }
18683 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
18684 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18685 {
18686 self.advance(); // LATERAL
18687 self.advance(); // (
18688 // Parse the inner SELECT.
18689 let inner = match self.parse_one_statement()? {
18690 Statement::Select(s) => s,
18691 other => {
18692 return Err(self.err(alloc::format!(
18693 "expected SELECT inside LATERAL ( … ), got {other:?}"
18694 )));
18695 }
18696 };
18697 if !matches!(self.peek(), Token::RParen) {
18698 return Err(self.err(alloc::format!(
18699 "expected ')' after LATERAL subquery, got {:?}",
18700 self.peek()
18701 )));
18702 }
18703 self.advance();
18704 // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
18705 // `(VALUES …) t(g)` derived table round-trips through view-body
18706 // Display, which renders on the lateral_subquery channel).
18707 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18708 let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
18709 return Ok(TableRef {
18710 name,
18711 alias: alias_ident,
18712 only: false,
18713 as_of_segment: None,
18714 unnest_expr: None,
18715 unnest_column_aliases: column_aliases,
18716 with_ordinality: false,
18717 generate_series_args: None,
18718 lateral_subquery: Some(Box::new(inner)),
18719 jsonb_each_text_arg: None,
18720 table_fn_call: None,
18721 rows_from: None,
18722 json_table: None,
18723 scalar_fn_item: false,
18724 });
18725 }
18726 // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
18727 // function as a FROM item. Emits one row per (key, value)
18728 // pair in the JSONB object argument as TEXT columns. May
18729 // be wrapped in CROSS JOIN LATERAL when the argument
18730 // references a preceding FROM item (sentori migration
18731 // 0067 backfill shape: `CROSS JOIN LATERAL
18732 // jsonb_each_text(t.json_col) AS kv(key, value)`).
18733 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
18734 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18735 {
18736 let each_fn = match self.peek() {
18737 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18738 _ => unreachable!(),
18739 };
18740 self.advance(); // jsonb_each[_text] / json_each[_text]
18741 self.advance(); // (
18742 let arg = self.parse_expr(0)?;
18743 if !matches!(self.peek(), Token::RParen) {
18744 return Err(self.err(alloc::format!(
18745 "expected ')' after {each_fn}() argument, got {:?}",
18746 self.peek()
18747 )));
18748 }
18749 self.advance();
18750 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18751 let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
18752 return Ok(TableRef {
18753 name,
18754 alias: alias_ident,
18755 only: false,
18756 as_of_segment: None,
18757 unnest_expr: None,
18758 // `AS t(k, v)` renames key/value positionally, same as the
18759 // LATERAL-position form already does.
18760 unnest_column_aliases: column_aliases,
18761 with_ordinality: false,
18762 generate_series_args: None,
18763 lateral_subquery: None,
18764 jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
18765 table_fn_call: None,
18766 rows_from: None,
18767 json_table: None,
18768 scalar_fn_item: false,
18769 });
18770 }
18771 // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
18772 // (+ json_ variants) — record-returning JSON functions with a
18773 // column-definition list. Desugar to a derived table that
18774 // projects each declared column from the JSON via `->>` + a cast,
18775 // over `jsonb_array_elements(J)` for the *set (per-element) form.
18776 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
18777 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18778 {
18779 return self.parse_json_to_record_from();
18780 }
18781 // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
18782 // row is a text[] of capture groups, so it cannot desugar to unnest
18783 // (that would flatten the array). Wrap it as a derived table
18784 // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
18785 // SRF path already emits one text[] row per match. PG names the column
18786 // `regexp_matches`; an `AS a(col)` alias overrides it.
18787 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18788 if s.eq_ignore_ascii_case("regexp_matches"))
18789 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18790 {
18791 self.advance(); // fn name
18792 self.advance(); // (
18793 let mut fn_args: Vec<Expr> = Vec::new();
18794 loop {
18795 fn_args.push(self.parse_expr(0)?);
18796 if matches!(self.peek(), Token::Comma) {
18797 self.advance();
18798 continue;
18799 }
18800 break;
18801 }
18802 if !matches!(self.peek(), Token::RParen) {
18803 return Err(self.err(alloc::format!(
18804 "expected ')' after regexp_matches() arguments, got {:?}",
18805 self.peek()
18806 )));
18807 }
18808 self.advance();
18809 // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
18810 // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
18811 // it, so it died on the `with` token while every other table function
18812 // accepted it.
18813 let with_ordinality = self.absorb_with_ordinality();
18814 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18815 let table_alias = alias_ident
18816 .clone()
18817 .unwrap_or_else(|| "regexp_matches".to_string());
18818 // PG names a single-column function's output column after the ALIAS
18819 // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
18820 // `m` reads as that column and not as a whole-row composite. Naming
18821 // it after the function regardless made `SELECT m[1] FROM … AS m`
18822 // subscript a record.
18823 let col_name = column_aliases
18824 .first()
18825 .cloned()
18826 .or_else(|| alias_ident.clone())
18827 .unwrap_or_else(|| "regexp_matches".to_string());
18828 let inner = crate::ast::SelectStatement {
18829 locking: None,
18830 ctes: Vec::new(),
18831 distinct: false,
18832 distinct_on: Vec::new(),
18833 items: alloc::vec![SelectItem::Expr {
18834 expr: Expr::FunctionCall {
18835 name: "regexp_matches".to_string(),
18836 args: fn_args,
18837 },
18838 alias: Some(col_name),
18839 }],
18840 from: None,
18841 where_: None,
18842 group_by: None,
18843 group_by_all: false,
18844 having: None,
18845 unions: Vec::new(),
18846 order_by: Vec::new(),
18847 limit: None,
18848 offset: None,
18849 limit_with_ties: false,
18850 window_check_exprs: Vec::new(),
18851 };
18852 return Ok(TableRef {
18853 name: table_alias.clone(),
18854 alias: Some(table_alias),
18855 only: false,
18856 as_of_segment: None,
18857 unnest_expr: None,
18858 unnest_column_aliases: column_aliases,
18859 with_ordinality,
18860 generate_series_args: None,
18861 lateral_subquery: Some(Box::new(inner)),
18862 jsonb_each_text_arg: None,
18863 table_fn_call: None,
18864 rows_from: None,
18865 json_table: None,
18866 // regexp_matches returns text[], a base type: `SELECT m FROM
18867 // regexp_matches(…) AS m` is the array, not a composite wrapping it.
18868 scalar_fn_item: true,
18869 });
18870 }
18871 // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
18872 // / json_ variants as a FROM item. Rewritten into
18873 // `unnest(<same fn>(<expr>))`: the scalar form returns the
18874 // elements as a TEXT array, and the existing unnest SRF path
18875 // materialises one row per element. PG's natural column name
18876 // is `value`; an `AS a(col)` column-alias list overrides it.
18877 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
18878 if s.eq_ignore_ascii_case("jsonb_array_elements")
18879 || s.eq_ignore_ascii_case("json_array_elements")
18880 || s.eq_ignore_ascii_case("jsonb_array_elements_text")
18881 || s.eq_ignore_ascii_case("json_array_elements_text")
18882 || s.eq_ignore_ascii_case("jsonb_object_keys")
18883 || s.eq_ignore_ascii_case("json_object_keys")
18884 || s.eq_ignore_ascii_case("jsonb_path_query")
18885 || s.eq_ignore_ascii_case("json_path_query")
18886 || s.eq_ignore_ascii_case("generate_subscripts")
18887 || s.eq_ignore_ascii_case("string_to_table")
18888 || s.eq_ignore_ascii_case("regexp_split_to_table"))
18889 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
18890 {
18891 let fn_name = match self.peek() {
18892 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
18893 _ => unreachable!(),
18894 };
18895 self.advance(); // fn name
18896 self.advance(); // (
18897 let mut fn_args: Vec<Expr> = Vec::new();
18898 loop {
18899 fn_args.push(self.parse_expr(0)?);
18900 if matches!(self.peek(), Token::Comma) {
18901 self.advance();
18902 continue;
18903 }
18904 break;
18905 }
18906 if !matches!(self.peek(), Token::RParen) {
18907 return Err(self.err(alloc::format!(
18908 "expected ')' after {fn_name}() arguments, got {:?}",
18909 self.peek()
18910 )));
18911 }
18912 self.advance();
18913 let with_ordinality = self.absorb_with_ordinality();
18914 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
18915 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
18916 // PG's natural column name: the array-elements SRFs
18917 // declare an OUT parameter `value`; jsonb_object_keys
18918 // and generate_subscripts have none, so the column is
18919 // named after the function. A bare table alias on a
18920 // single-column SRF renames the column too (PG: `FROM
18921 // generate_subscripts(a, 1) AS s` projects column s) —
18922 // except for the OUT-parameter SRFs, whose column stays
18923 // `value` under a bare alias.
18924 let natural_col = if fn_name.ends_with("_array_elements")
18925 || fn_name.ends_with("_array_elements_text")
18926 {
18927 "value".to_string()
18928 } else {
18929 alias_ident.clone().unwrap_or_else(|| fn_name.clone())
18930 };
18931 let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
18932 // Keep any further entries — the second names the
18933 // ordinality column under WITH ORDINALITY.
18934 srf_cols.extend(column_aliases.into_iter().skip(1));
18935 // The *_to_table SRFs are row-streams over the existing
18936 // *_to_array scalars — map the call target; the display
18937 // name (alias / column defaults) keeps the SRF spelling.
18938 let call_name = match fn_name.as_str() {
18939 "string_to_table" => "string_to_array".to_string(),
18940 "regexp_split_to_table" => "regexp_split_to_array".to_string(),
18941 _ => fn_name,
18942 };
18943 // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
18944 // preceding FROM item (bare or qualified column) is correlated;
18945 // route it through the per-outer-row lateral channel.
18946 let expr = crate::ast::Expr::FunctionCall {
18947 name: call_name,
18948 args: fn_args,
18949 };
18950 let correlated = Self::expr_has_any_column(&expr);
18951 let tref = TableRef {
18952 name,
18953 alias: alias_ident,
18954 only: false,
18955 as_of_segment: None,
18956 unnest_expr: Some(Box::new(expr)),
18957 unnest_column_aliases: srf_cols,
18958 with_ordinality,
18959 generate_series_args: None,
18960 lateral_subquery: None,
18961 jsonb_each_text_arg: None,
18962 table_fn_call: None,
18963 rows_from: None,
18964 json_table: None,
18965 // Each of these returns a BASE type (jsonb / text / int), so the item's
18966 // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
18967 // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
18968 scalar_fn_item: !with_ordinality,
18969 };
18970 return Ok(if correlated {
18971 Self::wrap_correlated_srf(tref)
18972 } else {
18973 tref
18974 });
18975 }
18976 // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
18977 // explicit parallel-zip syntax. Each entry lowers to its
18978 // array-returning scalar form (unnest(x) → x itself; the
18979 // FROM-SRF rewrite family → their scalar array calls) and
18980 // the list rides the multi-arg unnest zip channel:
18981 // NULL-padded to the longest, WITH ORDINALITY appends the
18982 // counter. generate_series has no scalar array form and
18983 // errors honestly.
18984 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
18985 && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
18986 && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
18987 {
18988 self.advance(); // ROWS
18989 self.advance(); // FROM
18990 self.advance(); // (
18991 let mut entries: Vec<Expr> = Vec::new();
18992 // v7.39 (read01 round 74) — the generic channel, filled in parallel.
18993 // Used only when some entry has no array form.
18994 let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
18995 loop {
18996 let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
18997 if !matches!(self.peek(), Token::LParen) {
18998 return Err(self.err(alloc::format!(
18999 "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19000 self.peek()
19001 )));
19002 }
19003 self.advance();
19004 let mut fn_args: Vec<Expr> = Vec::new();
19005 if !matches!(self.peek(), Token::RParen) {
19006 loop {
19007 fn_args.push(self.parse_expr(0)?);
19008 if matches!(self.peek(), Token::Comma) {
19009 self.advance();
19010 continue;
19011 }
19012 break;
19013 }
19014 }
19015 if !matches!(self.peek(), Token::RParen) {
19016 return Err(self.err(alloc::format!(
19017 "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19018 self.peek()
19019 )));
19020 }
19021 self.advance();
19022 let entry = match fn_name.as_str() {
19023 "unnest" => {
19024 if fn_args.len() != 1 {
19025 return Err(
19026 self.err("unnest inside ROWS FROM takes exactly one array".into())
19027 );
19028 }
19029 fn_args.pop().expect("len checked")
19030 }
19031 "jsonb_array_elements"
19032 | "json_array_elements"
19033 | "jsonb_array_elements_text"
19034 | "json_array_elements_text"
19035 | "jsonb_object_keys"
19036 | "json_object_keys"
19037 | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19038 name: fn_name,
19039 args: fn_args,
19040 },
19041 "string_to_table" => crate::ast::Expr::FunctionCall {
19042 name: "string_to_array".to_string(),
19043 args: fn_args,
19044 },
19045 "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19046 name: "regexp_split_to_array".to_string(),
19047 args: fn_args,
19048 },
19049 // v7.39 (read01 round 74) — an SRF with no array form
19050 // (`generate_series`, a user `RETURNS SETOF` function) has no
19051 // scalar expression to zip, so the WHOLE list switches to the
19052 // rows_from channel, which runs each function and zips the
19053 // rows themselves. The all-array case keeps the old lowering:
19054 // it is well-trodden and this must not disturb it.
19055 _ => {
19056 generic.push((fn_name, fn_args));
19057 if matches!(self.peek(), Token::Comma) {
19058 self.advance();
19059 continue;
19060 }
19061 break;
19062 }
19063 };
19064 generic.push((
19065 // The array-able entries carry their lowered expr along, so a
19066 // MIXED list still works: the engine sees the scalar array
19067 // form and unnests it.
19068 "__array".to_string(),
19069 alloc::vec![entry.clone()],
19070 ));
19071 entries.push(entry);
19072 if matches!(self.peek(), Token::Comma) {
19073 self.advance();
19074 continue;
19075 }
19076 break;
19077 }
19078 if !matches!(self.peek(), Token::RParen) {
19079 return Err(self.err(alloc::format!(
19080 "expected ')' to close ROWS FROM, got {:?}",
19081 self.peek()
19082 )));
19083 }
19084 self.advance();
19085 let with_ordinality = self.absorb_with_ordinality();
19086 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19087 let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19088 // v7.39 (read01 round 74) — some entry had no array form, so the whole
19089 // list rides the generic channel.
19090 if generic.iter().any(|(n, _)| n != "__array") {
19091 let correlated = generic
19092 .iter()
19093 .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19094 let tref = TableRef {
19095 name,
19096 alias: alias_ident,
19097 only: false,
19098 as_of_segment: None,
19099 unnest_expr: None,
19100 unnest_column_aliases,
19101 with_ordinality,
19102 generate_series_args: None,
19103 lateral_subquery: None,
19104 jsonb_each_text_arg: None,
19105 table_fn_call: None,
19106 rows_from: Some(generic),
19107 json_table: None,
19108 scalar_fn_item: false,
19109 };
19110 return Ok(if correlated {
19111 Self::wrap_correlated_srf(tref)
19112 } else {
19113 tref
19114 });
19115 }
19116 let correlated = entries.iter().any(Self::expr_has_any_column);
19117 let expr = if entries.len() == 1 {
19118 entries.pop().expect("len checked")
19119 } else {
19120 crate::ast::Expr::FunctionCall {
19121 name: "__unnest_zip".to_string(),
19122 args: entries,
19123 }
19124 };
19125 let tref = TableRef {
19126 name,
19127 alias: alias_ident,
19128 only: false,
19129 as_of_segment: None,
19130 unnest_expr: Some(Box::new(expr)),
19131 unnest_column_aliases,
19132 with_ordinality,
19133 generate_series_args: None,
19134 lateral_subquery: None,
19135 jsonb_each_text_arg: None,
19136 table_fn_call: None,
19137 rows_from: None,
19138 json_table: None,
19139 scalar_fn_item: false,
19140 };
19141 return Ok(if correlated {
19142 Self::wrap_correlated_srf(tref)
19143 } else {
19144 tref
19145 });
19146 }
19147 // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19148 // source. Detect at the head before the bare-ident fallback;
19149 // unnest is not a reserved token.
19150 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19151 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19152 {
19153 self.advance(); // unnest
19154 self.advance(); // (
19155 let mut srf_args = alloc::vec![self.parse_expr(0)?];
19156 while matches!(self.peek(), Token::Comma) {
19157 self.advance();
19158 srf_args.push(self.parse_expr(0)?);
19159 }
19160 if !matches!(self.peek(), Token::RParen) {
19161 return Err(self.err(alloc::format!(
19162 "expected ')' after unnest() argument, got {:?}",
19163 self.peek()
19164 )));
19165 }
19166 self.advance();
19167 // Multi-arg unnest(a, b, …) zips the arrays in
19168 // parallel, NULL-padding to the longest (PG's ROWS
19169 // FROM shorthand). Lower onto the unnest channel as an
19170 // internal marker call the executors unpack.
19171 let expr = if srf_args.len() == 1 {
19172 srf_args.pop().expect("len checked")
19173 } else {
19174 crate::ast::Expr::FunctionCall {
19175 name: "__unnest_zip".to_string(),
19176 args: srf_args,
19177 }
19178 };
19179 let with_ordinality = self.absorb_with_ordinality();
19180 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19181 let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19182 let correlated = Self::expr_has_any_column(&expr);
19183 let tref = TableRef {
19184 name,
19185 alias: alias_ident,
19186 only: false,
19187 as_of_segment: None,
19188 unnest_expr: Some(Box::new(expr)),
19189 unnest_column_aliases,
19190 with_ordinality,
19191 generate_series_args: None,
19192 lateral_subquery: None,
19193 jsonb_each_text_arg: None,
19194 table_fn_call: None,
19195 rows_from: None,
19196 json_table: None,
19197 scalar_fn_item: false,
19198 };
19199 return Ok(if correlated {
19200 Self::wrap_correlated_srf(tref)
19201 } else {
19202 tref
19203 });
19204 }
19205 // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19206 // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19207 // generic table-fn arg parser can't read), so it is intercepted
19208 // here BEFORE the generic dispatch. The doc expr may reference
19209 // outer columns (implicit LATERAL) — same correlated-wrap rule.
19210 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19211 if s.eq_ignore_ascii_case("json_table"))
19212 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19213 {
19214 let tref = self.parse_json_table_ref()?;
19215 let correlated = tref
19216 .json_table
19217 .as_deref()
19218 .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19219 return Ok(if correlated {
19220 Self::wrap_correlated_srf(tref)
19221 } else {
19222 tref
19223 });
19224 }
19225 // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19226 // functions dispatched by name (`pg_partition_tree('t')`,
19227 // `pg_partition_ancestors('t')`). Same head-detection shape as
19228 // unnest; the engine executor owns the row shape per function.
19229 // v7.39 (read01 round 65) — and a USER function in FROM position
19230 // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19231 // (generate_series / unnest / the json_each family) keep it — their arms
19232 // sit further down, so they are excluded here by name rather than by
19233 // ordering. Anything else that is an ident followed by `(` is a table
19234 // function; the engine executor decides whether it is a builtin, a
19235 // set-returning user function, or an error.
19236 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19237 if !s.eq_ignore_ascii_case("generate_series")
19238 && !s.eq_ignore_ascii_case("unnest")
19239 && !is_json_each_name(s))
19240 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19241 {
19242 // Body out-of-line — this parse sits on the FROM/subquery
19243 // recursion chain (debug frame-cliff discipline).
19244 // v7.39 (read01 round 69) — a call whose arguments reference an outer
19245 // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19246 // outer row, so it rides the lateral channel. Same rule the unnest
19247 // arm uses.
19248 let tref = self.parse_table_fn_ref()?;
19249 let correlated = tref
19250 .table_fn_call
19251 .as_deref()
19252 .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19253 return Ok(if correlated {
19254 Self::wrap_correlated_srf(tref)
19255 } else {
19256 tref
19257 });
19258 }
19259 // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19260 // [, step])` set-returning source. Same shape as unnest:
19261 // detect at the head, parse the comma-separated arg list,
19262 // dispatch downstream through the engine's set-returning
19263 // path. Supports integer triplets (mailrs's `WITH row_no AS
19264 // (SELECT * FROM generate_series(1, N))` pattern) and
19265 // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19266 // date-range iteration pattern, which pre-3.10 had no
19267 // direct equivalent in SPG).
19268 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19269 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19270 {
19271 self.advance(); // generate_series
19272 self.advance(); // (
19273 let mut args: Vec<Expr> = Vec::new();
19274 loop {
19275 args.push(self.parse_expr(0)?);
19276 if matches!(self.peek(), Token::Comma) {
19277 self.advance();
19278 continue;
19279 }
19280 break;
19281 }
19282 if !matches!(self.peek(), Token::RParen) {
19283 return Err(self.err(alloc::format!(
19284 "expected ')' after generate_series() arguments, got {:?}",
19285 self.peek()
19286 )));
19287 }
19288 self.advance();
19289 if args.len() < 2 || args.len() > 3 {
19290 return Err(self.err(alloc::format!(
19291 "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19292 args.len()
19293 )));
19294 }
19295 let with_ordinality = self.absorb_with_ordinality();
19296 let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19297 let name = alias_ident
19298 .clone()
19299 .unwrap_or_else(|| "generate_series".to_string());
19300 let correlated = args.iter().any(Self::expr_has_any_column);
19301 let tref = TableRef {
19302 name,
19303 alias: alias_ident,
19304 only: false,
19305 as_of_segment: None,
19306 unnest_expr: None,
19307 unnest_column_aliases: column_aliases,
19308 with_ordinality,
19309 generate_series_args: Some(args),
19310 lateral_subquery: None,
19311 jsonb_each_text_arg: None,
19312 table_fn_call: None,
19313 rows_from: None,
19314 json_table: None,
19315 scalar_fn_item: false,
19316 };
19317 return Ok(if correlated {
19318 Self::wrap_correlated_srf(tref)
19319 } else {
19320 tref
19321 });
19322 }
19323 // v7.16.2 — preserve information_schema / pg_catalog
19324 // qualifiers (mailrs round-10 A.3). The generic
19325 // `expect_ident_like` strip silently drops the schema;
19326 // we want the engine to recognise these PG meta tables
19327 // and synthesise rows from the live catalog. Produce a
19328 // synthetic name (`__spg_info_columns` etc.) so the
19329 // engine's SELECT-side router can dispatch without
19330 // clashing with any user-defined `columns` table.
19331 let name = if let Some(synth) = self.try_peek_meta_qualified() {
19332 synth
19333 } else if let Some(synth) = self.try_peek_meta_bare() {
19334 synth
19335 } else {
19336 self.expect_ident_like()?
19337 };
19338 // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
19339 // time-travel clause. Parse BEFORE the alias so the
19340 // alias can still ride at the tail (`tbl AS OF SEGMENT
19341 // '5' alias`). `AS` is a reserved keyword token, while
19342 // `OF` and `SEGMENT` are bare idents.
19343 let as_of_segment = if matches!(self.peek(), Token::As)
19344 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
19345 {
19346 self.advance(); // AS
19347 self.advance(); // OF
19348 let kw = match self.peek().clone() {
19349 Token::Ident(s) | Token::QuotedIdent(s) => s,
19350 other => {
19351 return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
19352 }
19353 };
19354 if !kw.eq_ignore_ascii_case("segment") {
19355 return Err(self.err(format!(
19356 "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
19357 )));
19358 }
19359 self.advance();
19360 // Segment id literal — accept either a string or
19361 // integer for operator ergonomics.
19362 let id = match self.advance() {
19363 Token::String(s) => s
19364 .parse::<u32>()
19365 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19366 Token::Integer(n) => u32::try_from(n)
19367 .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
19368 other => {
19369 return Err(self.err(format!(
19370 "expected segment id literal after AS OF SEGMENT, got {other:?}"
19371 )));
19372 }
19373 };
19374 Some(id)
19375 } else {
19376 None
19377 };
19378 // TABLESAMPLE is not a reserved token — keep the bare-ident
19379 // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
19380 let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
19381 {
19382 None
19383 } else {
19384 self.parse_optional_alias()?
19385 };
19386 // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
19387 // (PG grammar). BERNOULLI lowers to a per-row
19388 // `random() < p/100` conjunct on the enclosing SELECT's
19389 // WHERE — exact row-level Bernoulli semantics. SYSTEM
19390 // shares the lowering: SPG has no page structure to
19391 // sample, and the row-level form returns the same expected
19392 // fraction. REPEATABLE(seed) promises a deterministic
19393 // sample SPG cannot honour yet — honest error rather than
19394 // a silently ignored seed.
19395 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
19396 self.advance();
19397 let method = self.expect_ident_like()?;
19398 if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
19399 return Err(self.err(alloc::format!(
19400 "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
19401 )));
19402 }
19403 if !matches!(self.peek(), Token::LParen) {
19404 return Err(self.err(alloc::format!(
19405 "expected '(' after TABLESAMPLE {}, got {:?}",
19406 method.to_ascii_uppercase(),
19407 self.peek()
19408 )));
19409 }
19410 self.advance();
19411 let percent = self.parse_expr(0)?;
19412 if !matches!(self.peek(), Token::RParen) {
19413 return Err(self.err(alloc::format!(
19414 "expected ')' after TABLESAMPLE percentage, got {:?}",
19415 self.peek()
19416 )));
19417 }
19418 self.advance();
19419 // REPEATABLE(seed) → a deterministic per-row draw seeded by
19420 // `seed`, so the sample is stable across repeats and rescans.
19421 // Non-REPEATABLE keeps the non-deterministic `random()` draw.
19422 let mut sample_seed: Option<Expr> = None;
19423 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
19424 self.advance();
19425 if !matches!(self.peek(), Token::LParen) {
19426 return Err(self.err(alloc::format!(
19427 "expected '(' after REPEATABLE, got {:?}",
19428 self.peek()
19429 )));
19430 }
19431 self.advance();
19432 let seed = self.parse_expr(0)?;
19433 if !matches!(self.peek(), Token::RParen) {
19434 return Err(self.err(alloc::format!(
19435 "expected ')' after REPEATABLE seed, got {:?}",
19436 self.peek()
19437 )));
19438 }
19439 self.advance();
19440 sample_seed = Some(seed);
19441 }
19442 let draw = match sample_seed {
19443 Some(seed) => Expr::FunctionCall {
19444 name: "__tsm_fract".to_string(),
19445 args: alloc::vec![seed],
19446 },
19447 None => Expr::FunctionCall {
19448 name: "random".to_string(),
19449 args: Vec::new(),
19450 },
19451 };
19452 self.pending_sample_preds.push(Expr::Binary {
19453 lhs: Box::new(draw),
19454 op: crate::ast::BinOp::Lt,
19455 rhs: Box::new(Expr::Binary {
19456 lhs: Box::new(percent),
19457 op: crate::ast::BinOp::Div,
19458 rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
19459 }),
19460 });
19461 }
19462 Ok(TableRef {
19463 name,
19464 alias,
19465 only,
19466 as_of_segment,
19467 unnest_expr: None,
19468 unnest_column_aliases: Vec::new(),
19469 with_ordinality: false,
19470 generate_series_args: None,
19471 lateral_subquery: None,
19472 jsonb_each_text_arg: None,
19473 table_fn_call: None,
19474 rows_from: None,
19475 json_table: None,
19476 scalar_fn_item: false,
19477 })
19478 }
19479
19480 /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
19481 /// but also accepts `AS alias(col [, col, …])` — the
19482 /// PG-standard table-function column-list form. The column
19483 /// list is only honoured when paired with `UNNEST(...)` in
19484 /// the parent; other call sites currently discard it.
19485 /// True when the expression tree contains a qualified column
19486 /// reference (`t.col`) — the syntactic marker that an SRF
19487 /// argument correlates with a preceding FROM item.
19488 fn expr_has_qualified_column(e: &Expr) -> bool {
19489 match e {
19490 Expr::Column(c) => c.qualifier.is_some(),
19491 Expr::Binary { lhs, rhs, .. } => {
19492 Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
19493 }
19494 Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
19495 Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
19496 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
19497 Expr::Case {
19498 operand,
19499 branches,
19500 else_branch,
19501 } => {
19502 operand
19503 .as_deref()
19504 .is_some_and(Self::expr_has_qualified_column)
19505 || branches.iter().any(|(w, t)| {
19506 Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
19507 })
19508 || else_branch
19509 .as_deref()
19510 .is_some_and(Self::expr_has_qualified_column)
19511 }
19512 _ => false,
19513 }
19514 }
19515
19516 /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
19517 /// counts a bare (unqualified) column. A set-returning function has no
19518 /// input columns of its own, so ANY column in its arguments is an outer
19519 /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
19520 fn expr_has_any_column(e: &Expr) -> bool {
19521 match e {
19522 Expr::Column(_) => true,
19523 Expr::Binary { lhs, rhs, .. } => {
19524 Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
19525 }
19526 Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
19527 Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
19528 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
19529 // v7.39 (round 759, F31-B8b) — a column INSIDE an array
19530 // constructor or subscript fell to the `_ => false` arm, so
19531 // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
19532 // channel and the eager peer eval answered `column "x" does
19533 // not exist` (the substitution walker already recurses both
19534 // shapes; only this detector was blind to them).
19535 Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
19536 Expr::ArraySubscript { target, index } => {
19537 Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
19538 }
19539 Expr::Case {
19540 operand,
19541 branches,
19542 else_branch,
19543 } => {
19544 operand.as_deref().is_some_and(Self::expr_has_any_column)
19545 || branches
19546 .iter()
19547 .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
19548 || else_branch
19549 .as_deref()
19550 .is_some_and(Self::expr_has_any_column)
19551 }
19552 _ => false,
19553 }
19554 }
19555
19556 /// Wrap a correlated SRF table ref (`unnest(t.col)` /
19557 /// `generate_series(1, t.n)`) into the lateral_subquery
19558 /// channel: `SELECT * FROM <srf>` executes per outer row with
19559 /// outer references substituted (v7.37.43-T4.5 machinery).
19560 /// Uncorrelated SRFs stay on their plain channels.
19561 fn wrap_correlated_srf(srf: TableRef) -> TableRef {
19562 let name = srf.name.clone();
19563 let alias = srf.alias.clone();
19564 let inner = crate::ast::SelectStatement {
19565 locking: None,
19566 ctes: Vec::new(),
19567 distinct: false,
19568 distinct_on: Vec::new(),
19569 items: alloc::vec![crate::ast::SelectItem::Wildcard],
19570 from: Some(crate::ast::FromClause {
19571 primary: srf,
19572 joins: Vec::new(),
19573 }),
19574 where_: None,
19575 group_by: None,
19576 group_by_all: false,
19577 having: None,
19578 unions: Vec::new(),
19579 order_by: Vec::new(),
19580 limit: None,
19581 offset: None,
19582 limit_with_ties: false,
19583 window_check_exprs: Vec::new(),
19584 };
19585 TableRef {
19586 name,
19587 alias,
19588 only: false,
19589 as_of_segment: None,
19590 unnest_expr: None,
19591 unnest_column_aliases: Vec::new(),
19592 with_ordinality: false,
19593 generate_series_args: None,
19594 lateral_subquery: Some(Box::new(inner)),
19595 jsonb_each_text_arg: None,
19596 table_fn_call: None,
19597 rows_from: None,
19598 json_table: None,
19599 scalar_fn_item: false,
19600 }
19601 }
19602
19603 /// True when the expression tree contains an unresolved
19604 /// `OVER w` marker (see parse_over_clause).
19605 fn expr_has_named_window(e: &Expr) -> bool {
19606 match e {
19607 Expr::WindowFunction { partition_by, .. } => matches!(
19608 partition_by.as_slice(),
19609 [Expr::Column(c)] if matches!(
19610 c.qualifier.as_deref(),
19611 Some("__named_window__") | Some("__named_window_ref__")
19612 )
19613 ),
19614 Expr::Binary { lhs, rhs, .. } => {
19615 Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
19616 }
19617 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
19618 Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
19619 Expr::Case {
19620 operand,
19621 branches,
19622 else_branch,
19623 } => {
19624 operand.as_deref().is_some_and(Self::expr_has_named_window)
19625 || branches.iter().any(|(w, t)| {
19626 Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
19627 })
19628 || else_branch
19629 .as_deref()
19630 .is_some_and(Self::expr_has_named_window)
19631 }
19632 _ => false,
19633 }
19634 }
19635
19636 /// v7.39 (round 705) — the NAMES the expression references through the
19637 /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
19638 /// definitions nothing referenced. Traversal mirrors
19639 /// `expr_has_named_window` above.
19640 fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
19641 match e {
19642 Expr::WindowFunction { partition_by, .. } => {
19643 if let [Expr::Column(c)] = partition_by.as_slice()
19644 && matches!(
19645 c.qualifier.as_deref(),
19646 Some("__named_window__") | Some("__named_window_ref__")
19647 )
19648 {
19649 into.push(c.name.clone());
19650 }
19651 }
19652 Expr::Binary { lhs, rhs, .. } => {
19653 Self::collect_named_window_refs(lhs, into);
19654 Self::collect_named_window_refs(rhs, into);
19655 }
19656 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19657 Self::collect_named_window_refs(expr, into);
19658 }
19659 Expr::FunctionCall { args, .. } => {
19660 for a in args {
19661 Self::collect_named_window_refs(a, into);
19662 }
19663 }
19664 Expr::Case {
19665 operand,
19666 branches,
19667 else_branch,
19668 } => {
19669 if let Some(o) = operand.as_deref() {
19670 Self::collect_named_window_refs(o, into);
19671 }
19672 for (w, t) in branches {
19673 Self::collect_named_window_refs(w, into);
19674 Self::collect_named_window_refs(t, into);
19675 }
19676 if let Some(eb) = else_branch.as_deref() {
19677 Self::collect_named_window_refs(eb, into);
19678 }
19679 }
19680 _ => {}
19681 }
19682 }
19683
19684 /// Inline named-window definitions into the `OVER w` markers.
19685 /// An unknown name errors (PG: window "w" does not exist).
19686 #[allow(clippy::type_complexity)]
19687 fn substitute_named_windows(
19688 e: &mut Expr,
19689 defs: &[(
19690 String,
19691 (
19692 Vec<Expr>,
19693 Vec<(Expr, bool, Option<bool>)>,
19694 Option<WindowFrame>,
19695 ),
19696 )],
19697 ) -> Result<(), String> {
19698 match e {
19699 Expr::WindowFunction {
19700 partition_by,
19701 order_by,
19702 frame,
19703 ..
19704 } => {
19705 // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
19706 // from the bare `OVER w1` (a plain reference).
19707 let named = match partition_by.as_slice() {
19708 [Expr::Column(c)] => match c.qualifier.as_deref() {
19709 Some("__named_window__") => Some((c.name.clone(), false)),
19710 Some("__named_window_ref__") => Some((c.name.clone(), true)),
19711 _ => None,
19712 },
19713 _ => None,
19714 };
19715 if let Some((wname, is_copy)) = named {
19716 let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
19717 else {
19718 return Err(alloc::format!("window {wname:?} does not exist"));
19719 };
19720 if !is_copy {
19721 *partition_by = def.0.clone();
19722 *order_by = def.1.clone();
19723 *frame = def.2.clone();
19724 return Ok(());
19725 }
19726 // v7.39 (round 229) — PG's copy rules, probed against
19727 // 18.4: a copy inherits the partitioning, may supply an
19728 // ordering only when the base has none, and may not copy
19729 // a base that already carries a frame (its own frame
19730 // would be ambiguous with the inherited one).
19731 if !def.1.is_empty() && !order_by.is_empty() {
19732 return Err(alloc::format!(
19733 "cannot override ORDER BY clause of window \"{wname}\""
19734 ));
19735 }
19736 if def.2.is_some() {
19737 return Err(alloc::format!(
19738 "cannot copy window \"{wname}\" because it has a frame clause"
19739 ));
19740 }
19741 *partition_by = def.0.clone();
19742 if order_by.is_empty() {
19743 *order_by = def.1.clone();
19744 }
19745 }
19746 Ok(())
19747 }
19748 Expr::Binary { lhs, rhs, .. } => {
19749 Self::substitute_named_windows(lhs, defs)?;
19750 Self::substitute_named_windows(rhs, defs)
19751 }
19752 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19753 Self::substitute_named_windows(expr, defs)
19754 }
19755 Expr::FunctionCall { args, .. } => {
19756 for a in args {
19757 Self::substitute_named_windows(a, defs)?;
19758 }
19759 Ok(())
19760 }
19761 Expr::Case {
19762 operand,
19763 branches,
19764 else_branch,
19765 } => {
19766 if let Some(op) = operand {
19767 Self::substitute_named_windows(op, defs)?;
19768 }
19769 for (w, t) in branches {
19770 Self::substitute_named_windows(w, defs)?;
19771 Self::substitute_named_windows(t, defs)?;
19772 }
19773 if let Some(el) = else_branch {
19774 Self::substitute_named_windows(el, defs)?;
19775 }
19776 Ok(())
19777 }
19778 _ => Ok(()),
19779 }
19780 }
19781
19782 /// SQL-standard `TABLE name` shorthand — builds the equivalent
19783 /// `SELECT * FROM name` head. Callers own set-op chain / tail
19784 /// composition.
19785 fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
19786 debug_assert!(matches!(self.peek(), Token::Table));
19787 self.advance(); // TABLE
19788 let tname = self.expect_ident_like()?;
19789 Ok(SelectStatement {
19790 locking: None,
19791 ctes: Vec::new(),
19792 distinct: false,
19793 distinct_on: Vec::new(),
19794 items: alloc::vec![SelectItem::Wildcard],
19795 from: Some(FromClause {
19796 primary: TableRef {
19797 name: tname,
19798 alias: None,
19799 only: false,
19800 as_of_segment: None,
19801 unnest_expr: None,
19802 unnest_column_aliases: Vec::new(),
19803 with_ordinality: false,
19804 generate_series_args: None,
19805 lateral_subquery: None,
19806 jsonb_each_text_arg: None,
19807 table_fn_call: None,
19808 rows_from: None,
19809 json_table: None,
19810 scalar_fn_item: false,
19811 },
19812 joins: Vec::new(),
19813 }),
19814 where_: None,
19815 group_by: None,
19816 group_by_all: false,
19817 having: None,
19818 unions: Vec::new(),
19819 order_by: Vec::new(),
19820 limit: None,
19821 offset: None,
19822 limit_with_ties: false,
19823 window_check_exprs: Vec::new(),
19824 })
19825 }
19826
19827 /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
19828 /// variants) → a derived table that reads each declared column out of
19829 /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
19830 /// `jsonb_array_elements(J)` (one row per element, column `value`);
19831 /// the scalar *record form projects a single row straight off `J`.
19832 /// Rides the existing lateral-subquery channel, so no new executor or
19833 /// AST is needed.
19834 fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
19835 use crate::ast::{
19836 BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
19837 };
19838 let fn_name = match self.peek() {
19839 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19840 _ => unreachable!("caller guarded is_json_to_record_name"),
19841 };
19842 self.advance(); // fn name
19843 self.advance(); // (
19844 let mut arg = self.parse_expr(0)?;
19845 // populate_record(base, json): the base only carries the record
19846 // type here — the JSON argument is the second expression.
19847 let mut base: Option<Expr> = None;
19848 if matches!(self.peek(), Token::Comma) {
19849 self.advance();
19850 base = Some(arg);
19851 arg = self.parse_expr(0)?;
19852 }
19853 if !matches!(self.peek(), Token::RParen) {
19854 return Err(self.err(alloc::format!(
19855 "expected ')' after {fn_name}() argument, got {:?}",
19856 self.peek()
19857 )));
19858 }
19859 self.advance(); // )
19860 let is_set = fn_name.ends_with("recordset");
19861 // `[AS] alias ( col type [, …] )` column-definition list.
19862 if matches!(self.peek(), Token::As) {
19863 self.advance();
19864 }
19865 let alias_opt = match self.peek() {
19866 Token::Ident(s) | Token::QuotedIdent(s) => {
19867 let a = s.clone();
19868 self.advance();
19869 Some(a)
19870 }
19871 _ => None,
19872 };
19873 // v7.39 (read01 round 76) — the populate family's canonical PG
19874 // spelling carries no column list at all: the row shape comes from
19875 // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
19876 // j)`). The parser has no catalog, so hand the two arguments to the
19877 // engine's table-function channel, which does. Only `*_to_record*`
19878 // (whose base is bare `record`) genuinely requires the list.
19879 if !matches!(self.peek(), Token::LParen) {
19880 if let Some(base_expr) = base {
19881 let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
19882 return Ok(TableRef {
19883 name: alias.clone(),
19884 alias: Some(alias),
19885 only: false,
19886 as_of_segment: None,
19887 unnest_expr: None,
19888 unnest_column_aliases: Vec::new(),
19889 with_ordinality: false,
19890 generate_series_args: None,
19891 lateral_subquery: None,
19892 jsonb_each_text_arg: None,
19893 table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
19894 rows_from: None,
19895 json_table: None,
19896 scalar_fn_item: false,
19897 });
19898 }
19899 return Err(self.err(alloc::format!(
19900 "expected '(' to start the {fn_name} column-definition list, got {:?}",
19901 self.peek()
19902 )));
19903 }
19904 let Some(alias) = alias_opt else {
19905 return Err(self.err(alloc::format!(
19906 "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
19907 )));
19908 };
19909 self.advance(); // (
19910 let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
19911 loop {
19912 let col = self.expect_ident_like()?;
19913 let ty = self.parse_cast_target()?;
19914 coldefs.push((col, ty));
19915 if matches!(self.peek(), Token::Comma) {
19916 self.advance();
19917 continue;
19918 }
19919 if matches!(self.peek(), Token::RParen) {
19920 self.advance();
19921 break;
19922 }
19923 return Err(self.err(alloc::format!(
19924 "expected ',' or ')' in {fn_name} column list, got {:?}",
19925 self.peek()
19926 )));
19927 }
19928 if coldefs.is_empty() {
19929 return Err(self.err(alloc::format!(
19930 "{fn_name} column-definition list must declare at least one column"
19931 )));
19932 }
19933 // Per column: (base ->> 'col')::type AS col. The base is the
19934 // per-element `value` column for the *set form, or the argument
19935 // itself for the scalar record form.
19936 let items: Vec<SelectItem> = coldefs
19937 .into_iter()
19938 .map(|(col, ty)| {
19939 let base = if is_set {
19940 Expr::Column(ColumnName {
19941 qualifier: None,
19942 name: "value".to_string(),
19943 })
19944 } else {
19945 arg.clone()
19946 };
19947 SelectItem::Expr {
19948 expr: Expr::Cast {
19949 expr: Box::new(Expr::Binary {
19950 lhs: Box::new(base),
19951 op: BinOp::JsonGetText,
19952 rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
19953 }),
19954 target: ty,
19955 },
19956 alias: Some(col),
19957 }
19958 })
19959 .collect();
19960 let from = if is_set {
19961 let elem_fn = if fn_name.starts_with("jsonb") {
19962 "jsonb_array_elements"
19963 } else {
19964 "json_array_elements"
19965 };
19966 Some(FromClause {
19967 primary: TableRef {
19968 name: "value".to_string(),
19969 alias: None,
19970 only: false,
19971 as_of_segment: None,
19972 unnest_expr: Some(Box::new(Expr::FunctionCall {
19973 name: elem_fn.to_string(),
19974 args: alloc::vec![arg],
19975 })),
19976 unnest_column_aliases: alloc::vec!["value".to_string()],
19977 with_ordinality: false,
19978 generate_series_args: None,
19979 lateral_subquery: None,
19980 jsonb_each_text_arg: None,
19981 table_fn_call: None,
19982 rows_from: None,
19983 json_table: None,
19984 scalar_fn_item: false,
19985 },
19986 joins: Vec::new(),
19987 })
19988 } else {
19989 None
19990 };
19991 let inner = SelectStatement {
19992 locking: None,
19993 ctes: Vec::new(),
19994 distinct: false,
19995 distinct_on: Vec::new(),
19996 items,
19997 from,
19998 where_: None,
19999 group_by: None,
20000 group_by_all: false,
20001 having: None,
20002 unions: Vec::new(),
20003 order_by: Vec::new(),
20004 limit: None,
20005 offset: None,
20006 limit_with_ties: false,
20007 window_check_exprs: Vec::new(),
20008 };
20009 Ok(TableRef {
20010 name: alias.clone(),
20011 alias: Some(alias),
20012 only: false,
20013 as_of_segment: None,
20014 unnest_expr: None,
20015 unnest_column_aliases: Vec::new(),
20016 with_ordinality: false,
20017 generate_series_args: None,
20018 lateral_subquery: Some(Box::new(inner)),
20019 jsonb_each_text_arg: None,
20020 table_fn_call: None,
20021 rows_from: None,
20022 json_table: None,
20023 scalar_fn_item: false,
20024 })
20025 }
20026
20027 /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20028 /// Returns true when the clause was present. `WITH` alone (a
20029 /// CTE can never start here) is not enough — the ORDINALITY
20030 /// ident must follow, so a stray WITH still errors downstream.
20031 fn absorb_with_ordinality(&mut self) -> bool {
20032 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20033 && matches!(self.tokens.get(self.pos + 1),
20034 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20035 {
20036 self.advance();
20037 self.advance();
20038 true
20039 } else {
20040 false
20041 }
20042 }
20043
20044 /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20045 /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20046 /// Out-of-line: the caller sits on the FROM recursion chain.
20047 #[inline(never)]
20048 fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20049 let fn_name = match self.advance() {
20050 Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20051 _ => unreachable!("caller peeked an ident"),
20052 };
20053 self.advance(); // (
20054 let mut args: Vec<Expr> = Vec::new();
20055 if !matches!(self.peek(), Token::RParen) {
20056 loop {
20057 args.push(self.parse_expr(0)?);
20058 if matches!(self.peek(), Token::Comma) {
20059 self.advance();
20060 continue;
20061 }
20062 break;
20063 }
20064 }
20065 if !matches!(self.peek(), Token::RParen) {
20066 return Err(self.err(alloc::format!(
20067 "expected ')' after {fn_name}() arguments, got {:?}",
20068 self.peek()
20069 )));
20070 }
20071 self.advance();
20072 // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20073 // counter column rides after the function's own, and the alias list
20074 // names it.
20075 let with_ordinality = self.absorb_with_ordinality();
20076 let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20077 let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20078 Ok(TableRef {
20079 name,
20080 alias: alias_ident,
20081 only: false,
20082 as_of_segment: None,
20083 unnest_expr: None,
20084 unnest_column_aliases,
20085 with_ordinality,
20086 generate_series_args: None,
20087 lateral_subquery: None,
20088 jsonb_each_text_arg: None,
20089 table_fn_call: Some(Box::new((fn_name, args))),
20090 rows_from: None,
20091 json_table: None,
20092 scalar_fn_item: false,
20093 })
20094 }
20095
20096 /// v7.39 (round 205, JSON_TABLE) — parse
20097 /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20098 /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20099 /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20100 #[inline(never)]
20101 fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20102 self.advance(); // json_table
20103 self.advance(); // (
20104 let doc = Box::new(self.parse_expr(0)?);
20105 self.expect_comma_json_table()?;
20106 let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20107 // Optional `PASSING <expr> AS <name> [, …]`.
20108 let mut passing: Vec<(String, Expr)> = Vec::new();
20109 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20110 self.advance();
20111 loop {
20112 let e = self.parse_expr(0)?;
20113 if !matches!(self.peek(), Token::As) {
20114 return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20115 }
20116 self.advance();
20117 let vname = match self.advance() {
20118 Token::Ident(s) | Token::QuotedIdent(s) => s,
20119 other => {
20120 return Err(self.err(alloc::format!(
20121 "expected PASSING variable name, got {other:?}"
20122 )));
20123 }
20124 };
20125 passing.push((vname, e));
20126 if matches!(self.peek(), Token::Comma) {
20127 self.advance();
20128 continue;
20129 }
20130 break;
20131 }
20132 }
20133 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20134 return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20135 }
20136 self.advance();
20137 let columns = self.parse_json_table_columns()?;
20138 if !matches!(self.peek(), Token::RParen) {
20139 return Err(self.err(alloc::format!(
20140 "expected ')' to close JSON_TABLE, got {:?}",
20141 self.peek()
20142 )));
20143 }
20144 self.advance();
20145 let alias_ident = self.parse_optional_alias()?;
20146 let name = alias_ident
20147 .clone()
20148 .unwrap_or_else(|| String::from("json_table"));
20149 Ok(TableRef {
20150 name,
20151 alias: alias_ident,
20152 only: false,
20153 as_of_segment: None,
20154 unnest_expr: None,
20155 unnest_column_aliases: Vec::new(),
20156 with_ordinality: false,
20157 generate_series_args: None,
20158 lateral_subquery: None,
20159 jsonb_each_text_arg: None,
20160 table_fn_call: None,
20161 rows_from: None,
20162 json_table: Some(Box::new(crate::ast::JsonTable {
20163 doc,
20164 row_path,
20165 columns,
20166 passing,
20167 })),
20168 scalar_fn_item: false,
20169 })
20170 }
20171
20172 fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20173 if !matches!(self.peek(), Token::Comma) {
20174 return Err(self.err(alloc::format!(
20175 "expected ',' after JSON_TABLE document, got {:?}",
20176 self.peek()
20177 )));
20178 }
20179 self.advance();
20180 Ok(())
20181 }
20182
20183 fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20184 match self.advance() {
20185 Token::String(s) => Ok(s),
20186 other => Err(self.err(alloc::format!(
20187 "expected {what} string literal, got {other:?}"
20188 ))),
20189 }
20190 }
20191
20192 /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20193 #[inline(never)]
20194 fn parse_json_table_columns(
20195 &mut self,
20196 ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20197 if !matches!(self.peek(), Token::LParen) {
20198 return Err(self.err("expected '(' after COLUMNS".into()));
20199 }
20200 self.advance();
20201 let mut cols = Vec::new();
20202 loop {
20203 cols.push(self.parse_json_table_one_column()?);
20204 if matches!(self.peek(), Token::Comma) {
20205 self.advance();
20206 continue;
20207 }
20208 break;
20209 }
20210 if !matches!(self.peek(), Token::RParen) {
20211 return Err(self.err(alloc::format!(
20212 "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20213 self.peek()
20214 )));
20215 }
20216 self.advance();
20217 Ok(cols)
20218 }
20219
20220 fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20221 use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20222 // NESTED [PATH] '<p>' COLUMNS (...)
20223 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20224 self.advance();
20225 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20226 self.advance();
20227 }
20228 let path = self.parse_json_string_literal("NESTED PATH")?;
20229 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20230 return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20231 }
20232 self.advance();
20233 let columns = self.parse_json_table_columns()?;
20234 return Ok(JsonTableColumn::Nested { path, columns });
20235 }
20236 // <name> ...
20237 let name = match self.advance() {
20238 Token::Ident(s) | Token::QuotedIdent(s) => s,
20239 other => {
20240 return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20241 }
20242 };
20243 // <name> FOR ORDINALITY
20244 if matches!(self.peek(), Token::For) {
20245 self.advance();
20246 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20247 return Err(self.err("expected ORDINALITY after FOR".into()));
20248 }
20249 self.advance();
20250 return Ok(JsonTableColumn::Ordinality { name });
20251 }
20252 // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20253 let ty = self.parse_column_type_name()?;
20254 let mut format_json = false;
20255 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20256 self.advance();
20257 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20258 return Err(self.err("expected JSON after FORMAT".into()));
20259 }
20260 self.advance();
20261 format_json = true;
20262 }
20263 let mut exists = false;
20264 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20265 self.advance();
20266 exists = true;
20267 }
20268 let mut path = alloc::format!("$.{name}");
20269 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20270 self.advance();
20271 path = self.parse_json_string_literal("column PATH")?;
20272 }
20273 if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20274 // `FORMAT JSON` after PATH (alternate placement).
20275 self.advance();
20276 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20277 self.advance();
20278 }
20279 format_json = true;
20280 }
20281 let mut wrapper = false;
20282 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20283 self.advance();
20284 // optional CONDITIONAL/UNCONDITIONAL
20285 if matches!(self.peek(), Token::Ident(s)
20286 if s.eq_ignore_ascii_case("unconditional")
20287 || s.eq_ignore_ascii_case("conditional"))
20288 {
20289 self.advance();
20290 }
20291 if !matches!(self.peek(), Token::Ident(s)
20292 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20293 {
20294 return Err(self.err("expected WRAPPER after WITH".into()));
20295 }
20296 self.advance();
20297 // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20298 if matches!(self.peek(), Token::Ident(s)
20299 if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20300 {
20301 self.advance();
20302 }
20303 wrapper = true;
20304 }
20305 // ON EMPTY / ON ERROR clauses (two, in any order).
20306 let mut on_empty = JsonTableOnBehavior::Null;
20307 let mut on_error = JsonTableOnBehavior::Null;
20308 for _ in 0..2 {
20309 let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20310 {
20311 self.advance();
20312 Some(JsonTableOnBehavior::Error)
20313 } else if matches!(self.peek(), Token::Null) {
20314 self.advance();
20315 Some(JsonTableOnBehavior::Null)
20316 } else if matches!(self.peek(), Token::Default) {
20317 self.advance();
20318 Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
20319 } else {
20320 None
20321 };
20322 let Some(behavior) = behavior else { break };
20323 // `ON {EMPTY|ERROR}`
20324 if !matches!(self.peek(), Token::On) {
20325 return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
20326 }
20327 self.advance();
20328 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
20329 self.advance();
20330 on_empty = behavior;
20331 } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
20332 self.advance();
20333 on_error = behavior;
20334 } else {
20335 return Err(self.err("expected EMPTY or ERROR after ON".into()));
20336 }
20337 }
20338 Ok(JsonTableColumn::Regular {
20339 name,
20340 ty,
20341 path,
20342 exists,
20343 format_json,
20344 wrapper,
20345 on_empty,
20346 on_error,
20347 })
20348 }
20349
20350 fn parse_optional_alias_with_columns(
20351 &mut self,
20352 ) -> Result<(Option<String>, Vec<String>), ParseError> {
20353 let alias = self.parse_optional_alias()?;
20354 if alias.is_none() {
20355 return Ok((None, Vec::new()));
20356 }
20357 let mut cols: Vec<String> = Vec::new();
20358 if matches!(self.peek(), Token::LParen) {
20359 self.advance();
20360 while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
20361 self.advance();
20362 cols.push(s);
20363 if matches!(self.peek(), Token::Comma) {
20364 self.advance();
20365 continue;
20366 }
20367 break;
20368 }
20369 if matches!(self.peek(), Token::RParen) {
20370 self.advance();
20371 }
20372 }
20373 Ok((alias, cols))
20374 }
20375
20376 /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
20377 /// whose keyword token was already consumed and whose `(` is the
20378 /// current token. Factored out of `parse_atom` (and marked
20379 /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
20380 /// recursive `parse_atom` frame — inlining them there enlarges the
20381 /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
20382 /// against, risking an overflow before the budget triggers.
20383 #[inline(never)]
20384 fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
20385 self.advance(); // (
20386 let mut args = Vec::new();
20387 if !matches!(self.peek(), Token::RParen) {
20388 loop {
20389 args.push(self.parse_expr(0)?);
20390 match self.peek() {
20391 Token::Comma => {
20392 self.advance();
20393 }
20394 Token::RParen => break,
20395 other => {
20396 return Err(self.err(alloc::format!(
20397 "expected ',' or ')' in {name}() args, got {other:?}"
20398 )));
20399 }
20400 }
20401 }
20402 }
20403 self.advance(); // )
20404 Ok(Expr::FunctionCall {
20405 name: name.into(),
20406 args,
20407 })
20408 }
20409
20410 /// FROM-clause: a primary table reference plus zero-or-more joined
20411 /// peers expressed via either `, <table>` (cross-product, no ON) or
20412 /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
20413 /// v1.10 keeps the join list flat (left-associative nested-loop
20414 /// semantics).
20415 fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
20416 let primary = self.parse_table_ref()?;
20417 let primary_qual = primary
20418 .alias
20419 .clone()
20420 .unwrap_or_else(|| primary.name.clone());
20421 let joins = self.parse_from_joins(&primary_qual)?;
20422 Ok(FromClause { primary, joins })
20423 }
20424
20425 /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
20426 /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
20427 /// SAME grammar after its target table has already been consumed.
20428 /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
20429 /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
20430 /// be parsed forward, once.)
20431 /// `left_primary_qual` is the qualifier (alias, else name) of whatever
20432 /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
20433 /// target in the MySQL multi-table form. It only feeds the `USING (…)`
20434 /// desugaring, which needs a name for the left side of each equality.
20435 fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
20436 let mut joins = Vec::new();
20437 loop {
20438 // `, <table>` — cross-product with no ON.
20439 if matches!(self.peek(), Token::Comma) {
20440 self.advance();
20441 let table = self.parse_table_ref()?;
20442 joins.push(FromJoin {
20443 kind: JoinKind::Cross,
20444 table,
20445 on: None,
20446 using_cols: None,
20447 natural: false,
20448 });
20449 continue;
20450 }
20451 // v7.37.16 — optional leading `NATURAL` before the join
20452 // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
20453 // not a lexer keyword (it arrives as a bare Ident), so match
20454 // it case-insensitively here. When present, no ON/USING
20455 // clause is allowed — the common columns are resolved at
20456 // execution time.
20457 let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
20458 if natural {
20459 self.advance();
20460 }
20461 // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
20462 // CROSS JOIN, and bare JOIN (defaults to INNER).
20463 let kind =
20464 match self.peek() {
20465 Token::Inner => {
20466 self.advance();
20467 if !matches!(self.peek(), Token::Join) {
20468 return Err(self
20469 .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
20470 }
20471 self.advance();
20472 JoinKind::Inner
20473 }
20474 Token::Left => {
20475 self.advance();
20476 if matches!(self.peek(), Token::Outer) {
20477 self.advance();
20478 }
20479 if !matches!(self.peek(), Token::Join) {
20480 return Err(self.err(format!(
20481 "expected JOIN after LEFT [OUTER], got {:?}",
20482 self.peek()
20483 )));
20484 }
20485 self.advance();
20486 JoinKind::Left
20487 }
20488 Token::Cross => {
20489 self.advance();
20490 if !matches!(self.peek(), Token::Join) {
20491 return Err(self
20492 .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
20493 }
20494 self.advance();
20495 JoinKind::Cross
20496 }
20497 // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
20498 Token::Right => {
20499 self.advance();
20500 if matches!(self.peek(), Token::Outer) {
20501 self.advance();
20502 }
20503 if !matches!(self.peek(), Token::Join) {
20504 return Err(self.err(format!(
20505 "expected JOIN after RIGHT [OUTER], got {:?}",
20506 self.peek()
20507 )));
20508 }
20509 self.advance();
20510 JoinKind::Right
20511 }
20512 // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
20513 Token::Full => {
20514 self.advance();
20515 if matches!(self.peek(), Token::Outer) {
20516 self.advance();
20517 }
20518 if !matches!(self.peek(), Token::Join) {
20519 return Err(self.err(format!(
20520 "expected JOIN after FULL [OUTER], got {:?}",
20521 self.peek()
20522 )));
20523 }
20524 self.advance();
20525 JoinKind::FullOuter
20526 }
20527 Token::Join => {
20528 self.advance();
20529 JoinKind::Inner
20530 }
20531 _ => break,
20532 };
20533 let table = self.parse_table_ref()?;
20534 // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
20535 // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
20536 // where prev_table is the most-recent left-side table
20537 // (the previous join's table if any, else the FROM primary).
20538 // PG semantics around column merging are richer (USING'd
20539 // cols become deduplicated single output columns); for
20540 // sugar purposes the predicate-only form covers the
20541 // baseline corpus shape and chained `… JOIN x USING (k)
20542 // JOIN y USING (k)` calls.
20543 // v7.37.16 — NATURAL joins carry no ON/USING clause; the
20544 // common columns resolve at execution time.
20545 if natural {
20546 joins.push(FromJoin {
20547 kind,
20548 table,
20549 on: None,
20550 using_cols: None,
20551 natural: true,
20552 });
20553 continue;
20554 }
20555 let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
20556 // v7.37.16 — capture the USING column list (in addition to
20557 // the ON desugar below) so the executor can perform PG's
20558 // column-merge on the output side.
20559 let mut using_cols: Option<Vec<String>> = None;
20560 let on = if matches!(self.peek(), Token::On) {
20561 self.advance();
20562 Some(self.parse_expr(0)?)
20563 } else if using_match {
20564 self.advance();
20565 if !matches!(self.peek(), Token::LParen) {
20566 return Err(
20567 self.err(format!("expected '(' after USING, got {:?}", self.peek()))
20568 );
20569 }
20570 self.advance();
20571 let mut cols: Vec<String> = Vec::new();
20572 loop {
20573 match self.peek().clone() {
20574 Token::Ident(s) | Token::QuotedIdent(s) => {
20575 self.advance();
20576 cols.push(s);
20577 }
20578 other => {
20579 return Err(self.err(format!(
20580 "expected column name inside USING (…), got {other:?}"
20581 )));
20582 }
20583 }
20584 match self.peek() {
20585 Token::Comma => {
20586 self.advance();
20587 continue;
20588 }
20589 Token::RParen => {
20590 self.advance();
20591 break;
20592 }
20593 other => {
20594 return Err(self.err(format!(
20595 "expected ',' or ')' inside USING (…), got {other:?}"
20596 )));
20597 }
20598 }
20599 }
20600 if cols.is_empty() {
20601 return Err(self.err("USING (…) requires at least one column".to_string()));
20602 }
20603 using_cols = Some(cols.clone());
20604 // Pick the left-side alias: prev join's table if any,
20605 // else FROM primary. Use alias when present, else
20606 // table name (PG-equivalent qualifier).
20607 let left_qual: String = joins
20608 .last()
20609 .map(|j| {
20610 j.table
20611 .alias
20612 .clone()
20613 .unwrap_or_else(|| j.table.name.clone())
20614 })
20615 .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
20616 let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
20617 let mut iter = cols.into_iter().map(|c| Expr::Binary {
20618 lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20619 qualifier: Some(left_qual.clone()),
20620 name: c.clone(),
20621 })),
20622 op: crate::ast::BinOp::Eq,
20623 rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
20624 qualifier: Some(right_qual.clone()),
20625 name: c,
20626 })),
20627 });
20628 let first = iter.next().expect("at least one col");
20629 Some(iter.fold(first, |acc, pred| Expr::Binary {
20630 lhs: alloc::boxed::Box::new(acc),
20631 op: crate::ast::BinOp::And,
20632 rhs: alloc::boxed::Box::new(pred),
20633 }))
20634 } else if kind == JoinKind::Cross {
20635 None
20636 } else {
20637 return Err(self.err(format!(
20638 "expected ON or USING after {:?} JOIN, got {:?}",
20639 kind,
20640 self.peek()
20641 )));
20642 };
20643 joins.push(FromJoin {
20644 kind,
20645 table,
20646 on,
20647 using_cols,
20648 natural: false,
20649 });
20650 }
20651 Ok(joins)
20652 }
20653
20654 /// Optional alias after an expression or table:
20655 /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
20656 /// accepted (PG-style implicit alias). Returns `None` if the next token
20657 /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
20658 fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
20659 if matches!(self.peek(), Token::As) {
20660 self.advance();
20661 // v7.39 (round 340, V56) — after AS the next token MUST be an
20662 // identifier. This used to return None and "let the caller
20663 // surface the error on the next expectation", but when AS is
20664 // the LAST token there is no next expectation: `SELECT 1 AS`
20665 // parsed clean and silently dropped the alias. PG rejects it.
20666 if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
20667 return self.expect_ident_like().map(Some);
20668 }
20669 return Err(self.err(alloc::format!(
20670 "expected an alias after AS, got {:?}",
20671 self.peek()
20672 )));
20673 }
20674 // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
20675 // grammar reserves a long list of follow-keywords from the
20676 // alias slot. SPG's bareword approximation: skip a small
20677 // set of idents that would otherwise be swallowed as the
20678 // table alias and break trailing clauses like CREATE
20679 // MATERIALIZED VIEW … WITH [NO] DATA or future ON
20680 // CONFLICT WHERE shapes.
20681 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
20682 if is_alias_stopword(s) {
20683 return Ok(None);
20684 }
20685 return Ok(self.expect_ident_like().ok());
20686 }
20687 Ok(None)
20688 }
20689
20690 /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
20691 fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
20692 // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
20693 // error beats a stack overflow (an overflow aborts the
20694 // embedding host process).
20695 self.enter_nested()?;
20696 let r = self.parse_expr_inner(min_prec);
20697 self.nest_depth -= 1;
20698 r
20699 }
20700
20701 /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
20702 /// When the upcoming tokens form one, return the underlying
20703 /// operator token and the position just past the closing paren
20704 /// so the binary loop can dispatch on the plain operator.
20705 fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
20706 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
20707 return None;
20708 }
20709 if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
20710 return None;
20711 }
20712 let mut i = self.pos + 2;
20713 // Optional schema qualifier (pg_catalog.<op> etc.).
20714 if matches!(self.tokens.get(i), Some(Token::Ident(_)))
20715 && matches!(self.tokens.get(i + 1), Some(Token::Dot))
20716 {
20717 i += 2;
20718 }
20719 let op_tok = self.tokens.get(i)?.clone();
20720 if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
20721 return None;
20722 }
20723 Some((i + 2, op_tok))
20724 }
20725
20726 /// PG operator symbols that lower onto function calls in
20727 /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
20728 /// family → regexp_like, comparison rung), `^@` (starts_with,
20729 /// comparison rung), `^` (power, tighter than `*`), `#`
20730 /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
20731 /// subset of the OR bits so the subtraction never borrows).
20732 fn try_symbol_operator(
20733 &mut self,
20734 lhs: &Expr,
20735 min_prec: u8,
20736 ) -> Result<Option<Expr>, ParseError> {
20737 enum Sym {
20738 Regex { ci: bool, negated: bool },
20739 Like { ci: bool, negated: bool },
20740 StartsWith,
20741 Power,
20742 Xor,
20743 RangeAdjacent,
20744 }
20745 // v7.39 (IS-precedence knife) — the low-precedence postfix
20746 // predicates ride this existing leaf call (zero new frame slots
20747 // on the nesting chain).
20748 if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
20749 return Ok(Some(e));
20750 }
20751 let (sym, prec): (Sym, u8) = match self.peek() {
20752 Token::Tilde => (
20753 Sym::Regex {
20754 ci: false,
20755 negated: false,
20756 },
20757 5,
20758 ),
20759 Token::TildeStar => (
20760 Sym::Regex {
20761 ci: true,
20762 negated: false,
20763 },
20764 5,
20765 ),
20766 Token::NotTilde => (
20767 Sym::Regex {
20768 ci: false,
20769 negated: true,
20770 },
20771 5,
20772 ),
20773 Token::NotTildeStar => (
20774 Sym::Regex {
20775 ci: true,
20776 negated: true,
20777 },
20778 5,
20779 ),
20780 // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
20781 Token::DoubleTilde => (
20782 Sym::Like {
20783 ci: false,
20784 negated: false,
20785 },
20786 5,
20787 ),
20788 Token::DoubleTildeStar => (
20789 Sym::Like {
20790 ci: true,
20791 negated: false,
20792 },
20793 5,
20794 ),
20795 Token::NotDoubleTilde => (
20796 Sym::Like {
20797 ci: false,
20798 negated: true,
20799 },
20800 5,
20801 ),
20802 Token::NotDoubleTildeStar => (
20803 Sym::Like {
20804 ci: true,
20805 negated: true,
20806 },
20807 5,
20808 ),
20809 Token::CaretAt => (Sym::StartsWith, 5),
20810 // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
20811 // tighter than `* / & |`, which the prec-9 rung preserves —
20812 // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
20813 Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
20814 Token::Caret => (Sym::Power, 9),
20815 // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
20816 // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
20817 Token::Hash => (Sym::Xor, 6),
20818 Token::Adjacent => (Sym::RangeAdjacent, 5),
20819 _ => return Ok(None),
20820 };
20821 if prec < min_prec {
20822 return Ok(None);
20823 }
20824 self.advance();
20825 let rhs = self.parse_expr(prec + 1)?;
20826 let out = match sym {
20827 Sym::Regex { ci, negated } => {
20828 let mut args = alloc::vec![lhs.clone(), rhs];
20829 if ci {
20830 args.push(Expr::Literal(Literal::String(String::from("i"))));
20831 }
20832 maybe_not(
20833 Expr::FunctionCall {
20834 name: String::from("regexp_like"),
20835 args,
20836 },
20837 negated,
20838 )
20839 }
20840 Sym::Like { ci, negated } => Expr::Like {
20841 expr: alloc::boxed::Box::new(lhs.clone()),
20842 pattern: alloc::boxed::Box::new(rhs),
20843 negated,
20844 case_insensitive: ci,
20845 },
20846 Sym::StartsWith => Expr::FunctionCall {
20847 name: String::from("starts_with"),
20848 args: alloc::vec![lhs.clone(), rhs],
20849 },
20850 Sym::Power => Expr::FunctionCall {
20851 name: String::from("power"),
20852 args: alloc::vec![lhs.clone(), rhs],
20853 },
20854 // `#` bitwise XOR — a real operator now (was desugared to
20855 // `(a|b)-(a&b)`, algebraically identical for integers but
20856 // undefined for bit strings; the direct op handles both).
20857 Sym::Xor => Expr::Binary {
20858 lhs: Box::new(lhs.clone()),
20859 op: BinOp::BitXor,
20860 rhs: Box::new(rhs),
20861 },
20862 // range `-|-` "is adjacent to" — lowered to a catalog function.
20863 Sym::RangeAdjacent => Expr::FunctionCall {
20864 name: String::from("range_adjacent"),
20865 args: alloc::vec![lhs.clone(), rhs],
20866 },
20867 };
20868 Ok(Some(out))
20869 }
20870
20871 /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
20872 /// predicates, moved out of the tight postfix-cast loop: PG binds
20873 /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
20874 /// looser than EVERY binary operator (only NOT/AND/OR are looser),
20875 /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
20876 /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
20877 /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
20878 /// when nothing at this position belongs to the family. Out-of-line
20879 /// (`inline(never)`): the caller sits on the per-nesting-level frame
20880 /// chain that MAX_NEST_DEPTH is tuned against.
20881 #[inline(never)]
20882 fn parse_postfix_predicate(
20883 &mut self,
20884 lhs: &Expr,
20885 min_prec: u8,
20886 ) -> Result<Option<Expr>, ParseError> {
20887 // Reached through try_symbol_operator (an existing leaf call of
20888 // the binary loop) so NO new stack slots land on the per-nesting
20889 // frame chain; the lhs clones only when a predicate actually
20890 // consumes it.
20891 match self.peek() {
20892 // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
20893 // comparison family rung 5 (each +1 from the pre-XOR ladder).
20894 Token::Is if min_prec <= 4 => {}
20895 Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
20896 Token::Not
20897 if min_prec <= 5
20898 && matches!(
20899 self.tokens.get(self.pos + 1),
20900 Some(Token::Between | Token::In | Token::Like)
20901 ) => {}
20902 Token::Not | Token::Ident(_)
20903 if min_prec <= 5
20904 && (matches!(self.peek(), Token::Ident(s)
20905 if s.eq_ignore_ascii_case("ilike")
20906 || (self.mysql_dialect
20907 && (s.eq_ignore_ascii_case("regexp")
20908 || s.eq_ignore_ascii_case("rlike")))
20909 || (s.eq_ignore_ascii_case("similar")
20910 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
20911 || (matches!(self.peek(), Token::Not)
20912 && matches!(self.tokens.get(self.pos + 1),
20913 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
20914 || (self.mysql_dialect
20915 && (s.eq_ignore_ascii_case("regexp")
20916 || s.eq_ignore_ascii_case("rlike")))
20917 || s.eq_ignore_ascii_case("similar")))) => {}
20918 _ => return Ok(None),
20919 }
20920 let mut expr = lhs.clone();
20921 // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
20922 // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
20923 if min_prec <= 4 {
20924 if matches!(self.peek(), Token::Is) {
20925 self.advance();
20926 let negated = if matches!(self.peek(), Token::Not) {
20927 self.advance();
20928 true
20929 } else {
20930 false
20931 };
20932 // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
20933 // mailrs pg_dump.
20934 if matches!(self.peek(), Token::Distinct) {
20935 self.advance();
20936 if !matches!(self.peek(), Token::From) {
20937 return Err(self.err(format!(
20938 "expected FROM after IS{} DISTINCT, got {:?}",
20939 if negated { " NOT" } else { "" },
20940 self.peek()
20941 )));
20942 }
20943 self.advance();
20944 // Right-hand side: parse at the same precedence
20945 // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
20946 // groups as `x IS DISTINCT FROM (a + b)`.
20947 let rhs = self.parse_expr(5)?;
20948 let op = if negated {
20949 BinOp::IsNotDistinctFrom
20950 } else {
20951 BinOp::IsDistinctFrom
20952 };
20953 expr = Expr::Binary {
20954 op,
20955 lhs: Box::new(expr),
20956 rhs: Box::new(rhs),
20957 };
20958 {
20959 return Ok(Some(expr));
20960 }
20961 }
20962 // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
20963 // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
20964 // Lowers onto pg_is_json(x, kind); NOT wraps the
20965 // call in a logical negation.
20966 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20967 if s.eq_ignore_ascii_case("json"))
20968 {
20969 self.advance(); // JSON
20970 let kind = match self.peek() {
20971 Token::Ident(s) | Token::QuotedIdent(s)
20972 if matches!(
20973 s.to_ascii_lowercase().as_str(),
20974 "value" | "object" | "array" | "scalar"
20975 ) =>
20976 {
20977 let k = s.to_ascii_lowercase();
20978 self.advance();
20979 k
20980 }
20981 _ => "value".to_string(),
20982 };
20983 let call = Expr::FunctionCall {
20984 name: "pg_is_json".to_string(),
20985 args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
20986 };
20987 expr = if negated {
20988 Expr::Unary {
20989 op: UnOp::Not,
20990 expr: Box::new(call),
20991 }
20992 } else {
20993 call
20994 };
20995 {
20996 return Ok(Some(expr));
20997 }
20998 }
20999 // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21000 // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21001 // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21002 {
21003 let form_kw = match self.peek() {
21004 Token::Ident(s) | Token::QuotedIdent(s)
21005 if matches!(
21006 s.to_ascii_uppercase().as_str(),
21007 "NFC" | "NFD" | "NFKC" | "NFKD"
21008 ) && matches!(
21009 self.tokens.get(self.pos + 1),
21010 Some(Token::Ident(n) | Token::QuotedIdent(n))
21011 if n.eq_ignore_ascii_case("normalized")
21012 ) =>
21013 {
21014 Some(s.to_ascii_uppercase())
21015 }
21016 _ => None,
21017 };
21018 let bare_normalized = form_kw.is_none()
21019 && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21020 if s.eq_ignore_ascii_case("normalized"));
21021 if form_kw.is_some() || bare_normalized {
21022 if form_kw.is_some() {
21023 self.advance(); // form keyword
21024 }
21025 self.advance(); // NORMALIZED
21026 let mut args = alloc::vec![expr];
21027 if let Some(f) = form_kw {
21028 args.push(Expr::Literal(Literal::String(f)));
21029 }
21030 let call = Expr::FunctionCall {
21031 name: "is_normalized".to_string(),
21032 args,
21033 };
21034 expr = if negated {
21035 Expr::Unary {
21036 op: UnOp::Not,
21037 expr: Box::new(call),
21038 }
21039 } else {
21040 call
21041 };
21042 {
21043 return Ok(Some(expr));
21044 }
21045 }
21046 }
21047 // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21048 // three-valued boolean tests. IS TRUE/FALSE never
21049 // return NULL, so they lower to CASE forms whose
21050 // ELSE catches the NULL branch; IS UNKNOWN on a
21051 // boolean is exactly IS NULL.
21052 if matches!(self.peek(), Token::True | Token::False)
21053 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21054 {
21055 let tok = self.advance();
21056 let test = match tok {
21057 Token::True => Some(true),
21058 Token::False => Some(false),
21059 _ => None, // UNKNOWN
21060 };
21061 // v7.39 (round 328, V45) — kept as what the user
21062 // wrote. These used to be lowered here into `CASE` /
21063 // `IS NULL`; the semantics were right but the AST no
21064 // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21065 // was echoed back as
21066 // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21067 expr = Expr::BoolTest {
21068 expr: Box::new(expr),
21069 value: test,
21070 negated,
21071 };
21072 {
21073 return Ok(Some(expr));
21074 }
21075 }
21076 if !matches!(self.peek(), Token::Null) {
21077 return Err(self.err(format!(
21078 "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21079 if negated { " NOT" } else { "" },
21080 self.peek()
21081 )));
21082 }
21083 self.advance();
21084 expr = Expr::IsNull {
21085 expr: Box::new(expr),
21086 negated,
21087 };
21088 {
21089 return Ok(Some(expr));
21090 }
21091 }
21092 }
21093 // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21094 if min_prec <= 5 {
21095 // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21096 // Look one token ahead so a stray `NOT` not followed by any of
21097 // these flows through to the early return below untouched.
21098 let negated = if matches!(self.peek(), Token::Not) {
21099 let next = self.tokens.get(self.pos + 1);
21100 matches!(next, Some(Token::Between | Token::In | Token::Like))
21101 || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21102 || (self.mysql_dialect
21103 && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21104 || s.eq_ignore_ascii_case("similar"))
21105 } else {
21106 false
21107 };
21108 if negated {
21109 self.advance();
21110 }
21111 if matches!(self.peek(), Token::Between) {
21112 expr = self.parse_between_tail(expr, negated)?;
21113 {
21114 return Ok(Some(expr));
21115 }
21116 }
21117 if matches!(self.peek(), Token::In) {
21118 if self.suppress_in_tail && !negated {
21119 // POSITION(sub IN str) — IN belongs to the
21120 // enclosing function syntax; stop here.
21121 {
21122 return Ok(None);
21123 }
21124 }
21125 expr = self.parse_in_tail(expr, negated)?;
21126 {
21127 return Ok(Some(expr));
21128 }
21129 }
21130 // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21131 // lowers onto the internal __similar_to(expr, pat[, esc]) call
21132 // (the SQL→regex transform runs inside, in the backtracking-
21133 // friendly shape SPG's matcher needs).
21134 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21135 && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21136 {
21137 self.advance(); // SIMILAR
21138 self.advance(); // TO
21139 let pattern = self.parse_expr(6)?;
21140 let mut args = alloc::vec![expr, pattern];
21141 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21142 self.advance();
21143 args.push(self.parse_expr(6)?);
21144 }
21145 let call = Expr::FunctionCall {
21146 name: "__similar_to".to_string(),
21147 args,
21148 };
21149 expr = maybe_not(call, negated);
21150 {
21151 return Ok(Some(expr));
21152 }
21153 }
21154 if matches!(self.peek(), Token::Like) {
21155 self.advance();
21156 // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21157 if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21158 expr = q;
21159 {
21160 return Ok(Some(expr));
21161 }
21162 }
21163 // Pattern at the same precedence as other comparison RHSes —
21164 // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21165 let mut pattern = self.parse_expr(6)?;
21166 // `ESCAPE 'c'` — rewrite a literal pattern to the
21167 // default backslash escape at parse time. Custom
21168 // escapes on non-literal patterns would need
21169 // matcher support; error honestly.
21170 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21171 self.advance();
21172 let esc = self.parse_expr(6)?;
21173 pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21174 }
21175 expr = Expr::Like {
21176 expr: Box::new(expr),
21177 pattern: Box::new(pattern),
21178 negated,
21179 case_insensitive: false,
21180 };
21181 {
21182 return Ok(Some(expr));
21183 }
21184 }
21185 // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21186 // keyword reaches us as a plain identifier.
21187 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21188 self.advance();
21189 if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21190 expr = q;
21191 {
21192 return Ok(Some(expr));
21193 }
21194 }
21195 let pattern = self.parse_expr(6)?;
21196 expr = Expr::Like {
21197 expr: Box::new(expr),
21198 pattern: Box::new(pattern),
21199 negated,
21200 case_insensitive: true,
21201 };
21202 {
21203 return Ok(Some(expr));
21204 }
21205 }
21206 // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21207 // operator (RLIKE is the alias). It is a keyword, not `~`, and
21208 // matches case-insensitively under the default collation, so it
21209 // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21210 // `~*` operator uses, wrapped in NOT when negated.
21211 if self.mysql_dialect
21212 && matches!(self.peek(), Token::Ident(s)
21213 if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21214 {
21215 self.advance();
21216 let pattern = self.parse_expr(6)?;
21217 let call = Expr::FunctionCall {
21218 name: String::from("regexp_like"),
21219 args: alloc::vec![
21220 expr,
21221 pattern,
21222 Expr::Literal(Literal::String(String::from("i"))),
21223 ],
21224 };
21225 return Ok(Some(maybe_not(call, negated)));
21226 }
21227 }
21228 let _ = expr;
21229 Ok(None)
21230 }
21231
21232 fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21233 let mut lhs = self.parse_unary()?;
21234 let mut chain_len = 0usize;
21235 loop {
21236 // OPERATOR([schema.]op) reduces to its underlying
21237 // operator token before the normal dispatch.
21238 let explicit = self.peek_explicit_operator();
21239 let dispatch = match &explicit {
21240 Some((_, tok)) => self.binop_here(tok),
21241 None => self.binop_here(self.peek()),
21242 };
21243 let Some((op, prec)) = dispatch else {
21244 // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21245 // of the symbol family. `binop_here` answers None for them
21246 // because they lower onto function calls rather than a
21247 // BinOp, and the fallback below reads `self.peek()` — the
21248 // word OPERATOR, not the operator. `pg_dump` writes every
21249 // catalog predicate this way, so its first query failed
21250 // and no dump ran:
21251 //
21252 // AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21253 //
21254 // Collapsing the wrapper to the operator it names puts the
21255 // token where the fallback already looks.
21256 if let Some((next, op_tok)) = explicit {
21257 self.tokens.splice(self.pos..next, [op_tok]);
21258 }
21259 if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21260 lhs = e;
21261 chain_len += 1;
21262 if chain_len > MAX_BINARY_CHAIN {
21263 return Err(self.err(alloc::format!(
21264 "more than {MAX_BINARY_CHAIN} chained binary operators"
21265 )));
21266 }
21267 continue;
21268 }
21269 break;
21270 };
21271 if prec < min_prec {
21272 break;
21273 }
21274 // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21275 // iteratively but evaluates and drops recursively;
21276 // depth beyond the budget overflows worker stacks.
21277 chain_len += 1;
21278 if chain_len > MAX_BINARY_CHAIN {
21279 return Err(self.err(alloc::format!(
21280 "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21281 )));
21282 }
21283 match explicit {
21284 Some((end_pos, _)) => self.pos = end_pos,
21285 None => {
21286 self.advance();
21287 }
21288 }
21289 // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21290 // ANY is a bare ident; ALL is a reserved Token. Both
21291 // require an immediate `(` to disambiguate from
21292 // identifier columns named `any` / `all`.
21293 let any_kind = match self.peek() {
21294 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21295 Some(false)
21296 }
21297 Token::Ident(s) | Token::QuotedIdent(s)
21298 if (s.eq_ignore_ascii_case("any")
21299 || s.eq_ignore_ascii_case("some")
21300 || s.eq_ignore_ascii_case("all"))
21301 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21302 {
21303 Some(!s.eq_ignore_ascii_case("all"))
21304 }
21305 _ => None,
21306 };
21307 if let Some(is_any) = any_kind {
21308 lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21309 continue;
21310 }
21311 let rhs = self.parse_expr(prec + 1)?;
21312 lhs = Expr::Binary {
21313 lhs: Box::new(lhs),
21314 op,
21315 rhs: Box::new(rhs),
21316 };
21317 }
21318 Ok(lhs)
21319 }
21320
21321 /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
21322 /// and the array form.
21323 ///
21324 /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
21325 /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
21326 /// this block's `Expr` temporaries and four `format!` sites slots in
21327 /// that frame on every level of `((((1))))`, which never reaches it.
21328 #[inline(never)]
21329 fn parse_any_all_rhs(
21330 &mut self,
21331 lhs: Expr,
21332 op: BinOp,
21333 is_any: bool,
21334 ) -> Result<Expr, ParseError> {
21335 self.advance(); // ident
21336 self.advance(); // (
21337 // `x op ANY (SELECT …)` — the quantified-subquery
21338 // form. `= ANY` is exactly IN; the other operators
21339 // lower onto EXISTS over the subquery as a derived
21340 // table, comparing against its single projection
21341 // aliased __v (x's columns resolve correlated).
21342 // ALL is the negated-EXISTS complement; a NULL
21343 // element makes PG return NULL where this lowering
21344 // returns true — the NOT NULL column case (the
21345 // practical one) is exact.
21346 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
21347 // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
21348 // legal PG too (round-151 sibling). Out-of-line
21349 // (#[inline(never)] helper) — this sits on
21350 // parse_expr's recursive frame and the two-armed
21351 // SELECT temporary blew the nesting-budget stack.
21352 let mut sub = self.parse_any_all_select_body()?;
21353 if !matches!(self.peek(), Token::RParen) {
21354 return Err(self.err(alloc::format!(
21355 "expected ')' after ANY/ALL subquery, got {:?}",
21356 self.peek()
21357 )));
21358 }
21359 self.advance();
21360 if sub.items.len() != 1 {
21361 return Err(self.err(alloc::format!(
21362 "ANY/ALL subquery must return one column, got {}",
21363 sub.items.len()
21364 )));
21365 }
21366 if is_any && matches!(op, BinOp::Eq) {
21367 return Ok(Expr::InSubquery {
21368 expr: Box::new(lhs),
21369 subquery: Box::new(sub),
21370 negated: false,
21371 });
21372 }
21373 // The engine's subquery resolvers materialise
21374 // the single-column result into an ARRAY the
21375 // existing AnyAll three-valued eval consumes.
21376 return Ok(Expr::AnyAll {
21377 expr: Box::new(lhs),
21378 op,
21379 array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
21380 is_any,
21381 });
21382 }
21383 let arr = self.parse_expr(0)?;
21384 if !matches!(self.peek(), Token::RParen) {
21385 return Err(self.err(alloc::format!(
21386 "expected ')' after ANY/ALL argument, got {:?}",
21387 self.peek()
21388 )));
21389 }
21390 self.advance();
21391 Ok(Expr::AnyAll {
21392 expr: Box::new(lhs),
21393 op,
21394 array: Box::new(arr),
21395 is_any,
21396 })
21397 }
21398
21399 /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
21400 /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
21401 #[inline(never)]
21402 fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
21403 self.advance();
21404 let e = self.parse_expr(9)?;
21405 Ok(build_center_call(e))
21406 }
21407
21408 /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
21409 /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
21410 /// unary minus.
21411 ///
21412 /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
21413 /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
21414 /// the Expr-sized local stays out of that frame.
21415 #[inline(never)]
21416 fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21417 self.advance();
21418 let e = self.parse_expr(9)?;
21419 Ok(Expr::FunctionCall {
21420 name: alloc::string::String::from(name),
21421 args: alloc::vec![e],
21422 })
21423 }
21424
21425 /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
21426 /// (horizontal). Out-of-line from `parse_unary` (frame budget).
21427 #[inline(never)]
21428 fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
21429 self.advance();
21430 let e = self.parse_expr(9)?;
21431 Ok(Expr::FunctionCall {
21432 name: alloc::string::String::from(if vertical {
21433 "isvertical"
21434 } else {
21435 "ishorizontal"
21436 }),
21437 args: alloc::vec![e],
21438 })
21439 }
21440
21441 /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
21442 /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
21443 /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
21444 #[inline(never)]
21445 fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
21446 self.advance();
21447 let e = self.parse_expr(9)?;
21448 Ok(Expr::Cast {
21449 expr: Box::new(e),
21450 target: CastTarget::Named("binary".to_string()),
21451 })
21452 }
21453
21454 /// The prefix operators that share one shape: take the token, parse
21455 /// an operand at `prec`, wrap it.
21456 ///
21457 /// `#[inline(never)]`, and one function instead of five arms, for the
21458 /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
21459 /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
21460 /// debug build gives EVERY arm's locals a slot in the frame, whichever
21461 /// arm runs. `((((1))))` reaches none of these arms and was carrying
21462 /// five `Expr`-sized locals per level for them anyway.
21463 #[inline(never)]
21464 fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
21465 self.advance();
21466 let e = self.parse_expr(prec)?;
21467 Ok(Expr::Unary {
21468 op,
21469 expr: Box::new(e),
21470 })
21471 }
21472
21473 /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
21474 /// and separate from it because of the literal folding below and the
21475 /// `format!` temporaries that folding needs.
21476 #[inline(never)]
21477 fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
21478 self.advance();
21479 // v7.39 (round 549) — fold the sign into an integer literal that
21480 // only fits once it is negative.
21481 //
21482 // `9223372036854775808` is one past i64::MAX, so the lexer hands
21483 // it over as a NUMERIC and `-` on a numeric stays numeric. PG
21484 // folds the sign first, so `-9223372036854775808` is a bigint
21485 // there — and `-9223372036854775808 - 1` raises "bigint out of
21486 // range" where SPG quietly answered -9223372036854775809, a value
21487 // no bigint can hold. The arithmetic itself was already checked;
21488 // only the literal's type was wrong.
21489 if let Token::Numeric(lit) = self.peek()
21490 && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
21491 {
21492 self.advance();
21493 return Ok(Expr::Literal(Literal::Integer(folded)));
21494 }
21495 // Unary minus binds tighter than `*`/`/` (now at prec 7 after
21496 // `<->` slotted into 5 and arithmetic shifted up).
21497 let e = self.parse_expr(9)?;
21498 Ok(Expr::Unary {
21499 op: UnOp::Neg,
21500 expr: Box::new(e),
21501 })
21502 }
21503
21504 /// tsquery `!!` prefix negation, lowered to the catalog function.
21505 /// Binds like unary minus. Out-of-line for the frame reason on
21506 /// `parse_unary_op`.
21507 #[inline(never)]
21508 fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
21509 self.advance();
21510 let e = self.parse_expr(9)?;
21511 Ok(Expr::FunctionCall {
21512 name: String::from("tsquery_not"),
21513 args: alloc::vec![e],
21514 })
21515 }
21516
21517 fn parse_unary(&mut self) -> Result<Expr, ParseError> {
21518 match self.peek() {
21519 // NOT binds tighter than AND / XOR / OR but looser than
21520 // comparisons — its operand takes everything ≥ the comparison
21521 // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
21522 // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
21523 // was rung 3, behaviour-identical when 3 was unused; AND now
21524 // occupies 3, so this must be 4 to keep NOT tighter than AND.)
21525 Token::Not => self.parse_unary_op(UnOp::Not, 4),
21526 // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
21527 // The body is out-of-line: `parse_unary` is one of the three
21528 // frames the parser's MAX_NEST_DEPTH is tuned against, and an
21529 // inline arm here overflowed the native stack in
21530 // `nesting_budget_errors_cleanly` — the guard test caught it,
21531 // exactly as the eval-side cliff did in rounds 346 and 351.
21532 Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
21533 self.parse_binary_prefix()
21534 }
21535 // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
21536 // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
21537 // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
21538 Token::Bang => self.parse_unary_op(UnOp::Not, 9),
21539 Token::Minus => self.parse_prefix_minus(),
21540 // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
21541 // worked only because the lexer reads it as one signed literal;
21542 // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
21543 // PG18 and MariaDB take all of them. Binds like unary minus.
21544 Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
21545 // Bitwise NOT binds like unary minus.
21546 Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
21547 // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
21548 // "center of" operator; desugars to center(x). The whole arm
21549 // is out-of-line: parse_unary sits on the per-nesting-level
21550 // frame chain that MAX_NEST_DEPTH is tuned against, so no
21551 // Expr-sized local may live in this frame.
21552 Token::TsMatch => self.parse_prefix_center(),
21553 // v7.39 (round 508) — the prefix operators that are named
21554 // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
21555 // is length. Out-of-line for the same nesting-frame reason as
21556 // parse_prefix_center — parse_unary sits on the recursive cycle
21557 // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
21558 // live in this frame.
21559 Token::At => self.parse_prefix_call("abs"),
21560 Token::Hash => self.parse_prefix_call("npoints"),
21561 Token::AtMinusAt => self.parse_prefix_call("length"),
21562 // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
21563 // "is horizontal" (lseg / line); desugars to the existing
21564 // isvertical()/ishorizontal() functions. Out-of-line for the
21565 // same nesting-frame reason as parse_prefix_center.
21566 Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
21567 Token::GeomHoriz => self.parse_prefix_geom_axis(false),
21568 Token::DoubleBang => self.parse_prefix_tsquery_not(),
21569 _ => self.parse_atom(),
21570 }
21571 }
21572
21573 /// Parse a parenthesised scalar subquery body after the caller has consumed
21574 /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
21575 /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
21576 /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
21577 /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
21578 /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
21579 /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
21580 /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
21581 /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
21582 /// which sits on the recursive nesting-budget cycle (a few extra bytes there
21583 /// tips the deep-nesting test into a stack overflow).
21584 #[inline(never)]
21585 fn array_subquery_ahead(&self) -> bool {
21586 if !matches!(self.peek(), Token::LParen) {
21587 return false;
21588 }
21589 matches!(
21590 self.tokens.get(self.pos + 1),
21591 Some(Token::Select | Token::Values)
21592 ) || matches!(
21593 self.tokens.get(self.pos + 1),
21594 Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
21595 )
21596 }
21597
21598 /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
21599 /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
21600 /// locals stay off parse_atom's recursive frame (round 105).
21601 #[inline(never)]
21602 fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
21603 self.advance(); // consume `[`
21604 let mut items: Vec<Expr> = Vec::new();
21605 if !matches!(self.peek(), Token::RBracket) {
21606 loop {
21607 // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
21608 // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
21609 if matches!(self.peek(), Token::LBracket) {
21610 items.push(self.parse_array_bracket_body()?);
21611 } else {
21612 items.push(self.parse_expr(0)?);
21613 }
21614 match self.peek() {
21615 Token::Comma => {
21616 self.advance();
21617 }
21618 Token::RBracket => break,
21619 other => {
21620 return Err(self.err(alloc::format!(
21621 "expected ',' or ']' in ARRAY literal, got {other:?}"
21622 )));
21623 }
21624 }
21625 }
21626 }
21627 self.advance(); // consume `]`
21628 Ok(Expr::Array(items))
21629 }
21630
21631 /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
21632 /// is already consumed; the current token is `(`. Desugars to a scalar
21633 /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
21634 /// the subquery's single-column rows in order — reusing the existing
21635 /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
21636 /// keeps the large `Statement` local off parse_atom's recursive frame.
21637 #[inline(never)]
21638 fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
21639 self.advance(); // consume `(`
21640 let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
21641 if w.eq_ignore_ascii_case("with"));
21642 let sub = if is_with {
21643 self.advance(); // WITH
21644 self.parse_with_cte_then_select()?
21645 } else {
21646 self.parse_select_stmt()?
21647 };
21648 if !matches!(self.peek(), Token::RParen) {
21649 return Err(self.err(alloc::format!(
21650 "expected ')' to close ARRAY(subquery), got {:?}",
21651 self.peek()
21652 )));
21653 }
21654 self.advance(); // consume `)`
21655 // Reuse the parser to build the array_agg wrapper from the subquery's
21656 // canonical text — avoids hand-constructing the derived-table AST.
21657 let wrapper = alloc::format!(
21658 "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
21659 );
21660 let stmt = parse_statement(&wrapper)
21661 .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
21662 let Statement::Select(sel) = stmt else {
21663 return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
21664 };
21665 Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
21666 }
21667
21668 #[inline(never)]
21669 fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
21670 let inner = if is_with {
21671 self.advance(); // WITH
21672 self.parse_with_cte_then_select()?
21673 } else {
21674 self.parse_select_stmt()?
21675 };
21676 match self.advance() {
21677 Token::RParen => {
21678 let Statement::Select(s) = inner else {
21679 return Err(ParseError {
21680 message: "scalar subquery body must be a SELECT".into(),
21681 token_pos: self.consumed_pos(),
21682 });
21683 };
21684 Ok(Expr::ScalarSubquery(Box::new(s)))
21685 }
21686 other => Err(ParseError {
21687 message: format!("expected ')' after scalar subquery, got {other:?}"),
21688 token_pos: self.consumed_pos(),
21689 }),
21690 }
21691 }
21692
21693 /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
21694 /// literals. The lexer splits them into an ident + string; recombine
21695 /// here. Out-of-line and returning `Option` so `parse_atom` — the
21696 /// recursive frame the 768 KiB stack budget is tuned against — pays no
21697 /// frame for the `body` / `bits` strings and their char loops (the
21698 /// round-367 frame cliff, M20).
21699 #[inline(never)]
21700 fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
21701 let is_hex = match self.peek() {
21702 Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
21703 Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
21704 _ => return None,
21705 };
21706 if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
21707 return None;
21708 }
21709 self.advance();
21710 let Token::String(body) = self.advance() else {
21711 unreachable!("guarded above");
21712 };
21713 // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
21714 // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
21715 // (hex pairs, even count required — MariaDB errors on an odd
21716 // count); `b'1010'` packs its bits big-endian, left-padded to a
21717 // byte. Lower both onto the bytea cast.
21718 if self.mysql_dialect {
21719 if is_hex {
21720 if body.len() % 2 == 1 {
21721 return Some(Err(self.err(alloc::format!(
21722 "invalid hex string literal X'{body}': odd digit count"
21723 ))));
21724 }
21725 for c in body.chars() {
21726 if !c.is_ascii_hexdigit() {
21727 return Some(Err(
21728 self.err(alloc::format!("invalid hexadecimal digit {c:?} in X'…'"))
21729 ));
21730 }
21731 }
21732 return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
21733 }
21734 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21735 return Some(Err(
21736 self.err(alloc::format!("invalid binary digit {bad:?} in b'…'"))
21737 ));
21738 }
21739 return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
21740 }
21741 let bits = if is_hex {
21742 let mut out = String::with_capacity(body.len() * 4);
21743 for c in body.chars() {
21744 let Some(d) = c.to_digit(16) else {
21745 return Some(Err(self.err(alloc::format!(
21746 "invalid hexadecimal digit {c:?} in X'…' bit string"
21747 ))));
21748 };
21749 out.push_str(&alloc::format!("{d:04b}"));
21750 }
21751 out
21752 } else {
21753 if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
21754 return Some(Err(self.err(alloc::format!(
21755 "invalid binary digit {bad:?} in B'…' bit string"
21756 ))));
21757 }
21758 body
21759 };
21760 // Route through the postfix-cast loop so a chained cast like
21761 // `B'1010'::int` attaches onto the implicit `::bit` cast instead
21762 // of erroring at the `::`.
21763 // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
21764 // literal keeps its exact length, while an explicit `::bit` cast is
21765 // bit(1) with pad/truncate semantics (PG).
21766 Some(self.finish_postfix_casts(Expr::Cast {
21767 expr: Box::new(Expr::Literal(Literal::String(bits))),
21768 target: CastTarget::Named("__bit_literal".to_string()),
21769 }))
21770 }
21771
21772 fn parse_atom(&mut self) -> Result<Expr, ParseError> {
21773 if let Some(res) = self.try_parse_bit_string_literal() {
21774 return res;
21775 }
21776 let tok_pos = self.pos;
21777 match self.advance() {
21778 Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
21779 Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
21780 // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
21781 // carrying the source mantissa + scale so no precision is lost. A
21782 // literal too wide for i128 falls back to double precision.
21783 // Out-of-line (#[inline(never)]) — this arm sits on the
21784 // parse_expr recursion chain; its expansion locals must not
21785 // widen the recursive frame (debug frame-cliff discipline).
21786 Token::Numeric(s) => match numeric_token_to_literal(s) {
21787 Ok(lit) => Ok(Expr::Literal(lit)),
21788 Err(msg) => Err(self.err(msg)),
21789 },
21790 Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
21791 // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
21792 // (the lexer only emits this token in the MySQL dialect). Lower
21793 // onto the existing bytea cast; out-of-line to keep this arm off
21794 // the parse recursion frame.
21795 Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
21796 Token::True => Ok(Expr::Literal(Literal::Bool(true))),
21797 Token::False => Ok(Expr::Literal(Literal::Bool(false))),
21798 Token::Null => Ok(Expr::Literal(Literal::Null)),
21799 // v6.1.1 — `$N` placeholder. The actual Value lookup
21800 // happens in the engine eval path against the prepared-
21801 // statement bind buffer.
21802 Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
21803 Token::LParen => {
21804 // v4.10: `(SELECT ...)` in expression position is a
21805 // scalar subquery; otherwise it's a parenthesised
21806 // expression. Peek for SELECT keyword to dispatch.
21807 // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
21808 // lexes as Ident("with") (not a reserved token). The subquery body
21809 // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
21810 // so its large `Statement` local stays out of parse_atom's stack
21811 // frame — parse_atom is on the recursive `((…))` cycle and the
21812 // nesting budget is tuned to its frame size).
21813 let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21814 if s.eq_ignore_ascii_case("with"));
21815 if matches!(self.peek(), Token::Select) || is_with {
21816 self.parse_paren_scalar_subquery(is_with)
21817 } else {
21818 let e = self.parse_expr(0)?;
21819 // `(a, b, …)` — a row constructor. Valid only
21820 // in front of a comparison operator or [NOT]
21821 // IN; both expand at parse time (lexicographic
21822 // comparison / OR'd row equalities).
21823 if matches!(self.peek(), Token::Comma) {
21824 let mut row = alloc::vec![e];
21825 while matches!(self.peek(), Token::Comma) {
21826 self.advance();
21827 row.push(self.parse_expr(0)?);
21828 }
21829 if !matches!(self.peek(), Token::RParen) {
21830 return Err(self.err(alloc::format!(
21831 "expected ')' after row constructor, got {:?}",
21832 self.peek()
21833 )));
21834 }
21835 self.advance();
21836 // A bare `(a, b, …)` row constructor can carry postfix
21837 // (`::text`, `.field`) just like `ROW(a, b, …)`; the
21838 // early return here skips parse_atom's tail postfix
21839 // pass, so fold casts in explicitly. For the
21840 // comparison / predicate forms nothing postfix follows,
21841 // so this is a no-op.
21842 return self
21843 .parse_row_comparison_tail(row)
21844 .and_then(|e| self.finish_postfix_casts(e));
21845 }
21846 match self.advance() {
21847 Token::RParen => Ok(e),
21848 other => Err(ParseError {
21849 message: format!("expected ')', got {other:?}"),
21850 token_pos: self.consumed_pos(),
21851 }),
21852 }
21853 }
21854 }
21855 Token::LBracket => self.parse_vector_literal_body(),
21856 Token::Extract => self.parse_extract_atom(),
21857 Token::Interval => self.parse_interval_atom(),
21858 // `LEFT` / `RIGHT` are reserved-keyword tokens because the
21859 // grammar dedicates arms for `LEFT [OUTER] JOIN` /
21860 // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
21861 // expression position calling the PG `left(string, n)` /
21862 // `right(string, n)` function; rebuild the AST as a regular
21863 // function call so the engine's apply_function dispatch picks
21864 // it up. Delegated to a #[inline(never)] helper so its locals
21865 // don't bloat this recursive `parse_atom` frame (the nesting
21866 // budget in `enter_nested` is tuned to parse_atom's size).
21867 Token::Left if matches!(self.peek(), Token::LParen) => {
21868 self.parse_lr_string_function_call("left")
21869 }
21870 Token::Right if matches!(self.peek(), Token::LParen) => {
21871 self.parse_lr_string_function_call("right")
21872 }
21873 // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
21874 // token; we match on the bare ident. NOT is a token
21875 // (consumed in the comparison rung), but `EXISTS (...)`
21876 // at the top of an expression starts here.
21877 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
21878 self.parse_exists_atom(false)
21879 }
21880 // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
21881 // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
21882 // CASE is a bare ident; we dispatch on lowercase match.
21883 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
21884 self.parse_case_atom()
21885 }
21886 // v7.37.17 (17.6 siblings) — PG typed datetime literals:
21887 // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
21888 // '…'`. Lower onto the ::cast node so the existing
21889 // runtime text→date/timestamp paths do the parsing. The
21890 // string must follow immediately, else the ident stays a
21891 // plain column reference.
21892 Token::Ident(s)
21893 if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
21894 && matches!(self.peek(), Token::String(_)) =>
21895 {
21896 let target =
21897 typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
21898 let Token::String(lit) = self.advance() else {
21899 unreachable!("peek guaranteed a string token");
21900 };
21901 Ok(Expr::Cast {
21902 expr: Box::new(Expr::Literal(Literal::String(lit))),
21903 target,
21904 })
21905 }
21906 // v7.39 (round 221) — the SQL-standard long spellings:
21907 // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
21908 // TIME ZONE '…'`. Consume the modifier and lower to the same
21909 // typed-literal cast (`timetz` / `timestamptz` for WITH).
21910 Token::Ident(s)
21911 if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
21912 && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
21913 || w.eq_ignore_ascii_case("without"))
21914 && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
21915 && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
21916 && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
21917 {
21918 let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
21919 self.advance(); // WITH / WITHOUT
21920 self.advance(); // TIME
21921 self.advance(); // ZONE
21922 let Token::String(lit) = self.advance() else {
21923 unreachable!("guard checked a string token");
21924 };
21925 let base = s.to_ascii_lowercase();
21926 let target = match (base.as_str(), with_tz) {
21927 ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
21928 ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
21929 (_, true) => CastTarget::Timestamptz,
21930 (_, false) => CastTarget::Timestamp,
21931 };
21932 Ok(Expr::Cast {
21933 expr: Box::new(Expr::Literal(Literal::String(lit))),
21934 target,
21935 })
21936 }
21937 // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
21938 // gathers the subquery's single-column rows (in its row order)
21939 // into an array. Desugared to `array_agg` over the subquery as a
21940 // derived table; out-of-line to keep parse_atom's frame small (it
21941 // sits on the recursive nesting-budget cycle).
21942 Token::Ident(s) | Token::QuotedIdent(s)
21943 if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
21944 {
21945 self.parse_array_subquery()
21946 }
21947 // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
21948 // is not a reserved token; we match by case-insensitive
21949 // ident. The opening `[` must follow immediately. v7.39 (read01
21950 // round 105) — the body moved out-of-line so its `Vec`/loop locals
21951 // leave parse_atom's frame (which sits on the nesting-budget cycle).
21952 Token::Ident(s) | Token::QuotedIdent(s)
21953 if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
21954 {
21955 self.parse_array_literal_body()
21956 }
21957 // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
21958 // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
21959 // We special-case before the generic ident dispatch so
21960 // the AGAINST clause never reaches the function-call
21961 // loop (which would mis-read `(cols) AGAINST` as a
21962 // call with no trailing modifier). The shape is
21963 // rewritten to a Boolean OR over per-column
21964 // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
21965 // term)` so the existing FTS evaluator handles
21966 // semantics — the fulltext-GIN built at CREATE TABLE
21967 // time is currently a "real index that survives dump
21968 // round-trip"; the planner hook that actually uses
21969 // it for posting-list intersection lands in a later
21970 // sub-phase (Phase 2.2b) without touching this surface.
21971 Token::Ident(s) | Token::QuotedIdent(s)
21972 if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
21973 {
21974 self.parse_match_against_atom()
21975 }
21976 Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
21977 // v7.37.43-T4 — PG-unreserved keywords are legal column /
21978 // alias names in expression context too. `release` appears
21979 // in sentori `0003_partition_events.sql` as both a column
21980 // reference (SELECT … release …) and an INSERT column list
21981 // entry. Mirrors `expect_ident_like`'s expansion of the
21982 // identifier set.
21983 other if unreserved_keyword_text(&other).is_some() => {
21984 let s = unreserved_keyword_text(&other).unwrap();
21985 self.finish_ident_atom(s)
21986 }
21987 // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
21988 // only inside `SET` before, so `SELECT @@autocommit` — which
21989 // every MySQL connector asks at handshake — was a parse error.
21990 // MariaDB accepts the bare, `@@session.` and `@@global.`
21991 // spellings alike and answers from the session's own value.
21992 Token::SessionVar(v) => {
21993 // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
21994 // has nothing to do with a `@@` engine setting: its own
21995 // per-session namespace, and an unset one reads NULL instead
21996 // of raising. Stripping every `@` (as this did) made `@x` and
21997 // `@@x` the same node, so `SELECT @x` answered "Unknown
21998 // system variable".
21999 Ok(variable_ref_atom(&v))
22000 }
22001 other => Err(ParseError {
22002 message: format!("unexpected token {other:?} in expression"),
22003 token_pos: tok_pos,
22004 }),
22005 }
22006 // After parsing the atom, fold any postfix `::vector` casts.
22007 .and_then(|atom| self.finish_postfix_casts(atom))
22008 }
22009
22010 /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22011 /// Both bind tighter than any binary op.
22012 /// Shared cast-target parser for postfix `::TYPE` and the
22013 /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22014 /// If the next tokens are `( N )`, consume them and return the canonical
22015 /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22016 /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22017 fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22018 if !matches!(self.peek(), Token::LParen) {
22019 return None;
22020 }
22021 self.advance(); // (
22022 let n = match self.advance() {
22023 Token::Integer(n) => n,
22024 _ => return Some(base.to_string()), // malformed → drop precision
22025 };
22026 if matches!(self.peek(), Token::RParen) {
22027 self.advance();
22028 }
22029 Some(alloc::format!("{base}({n})"))
22030 }
22031
22032 fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22033 let target = match self.advance() {
22034 Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22035 "int" | "integer" | "int4" => {
22036 if matches!(self.peek(), Token::LBracket)
22037 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22038 {
22039 self.advance();
22040 self.advance();
22041 CastTarget::IntArray
22042 } else {
22043 CastTarget::Int
22044 }
22045 }
22046 "bigint" | "int8" => {
22047 if matches!(self.peek(), Token::LBracket)
22048 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22049 {
22050 self.advance();
22051 self.advance();
22052 CastTarget::BigIntArray
22053 } else {
22054 CastTarget::BigInt
22055 }
22056 }
22057 "float" | "double" => CastTarget::Float,
22058 "text" => {
22059 // v7.10.11 — `::TEXT[]` widens to TextArray.
22060 if matches!(self.peek(), Token::LBracket)
22061 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22062 {
22063 self.advance();
22064 self.advance();
22065 CastTarget::TextArray
22066 } else {
22067 CastTarget::Text
22068 }
22069 }
22070 "bool" | "boolean" => CastTarget::Bool,
22071 "vector" => CastTarget::Vector,
22072 "date" => CastTarget::Date,
22073 // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22074 // seconds precision through the Named path (the engine rounds
22075 // the sub-second field); bare `::timestamp` keeps the fast arm.
22076 "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22077 Some(named) => CastTarget::Named(named),
22078 None => CastTarget::Timestamp,
22079 },
22080 "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22081 Some(named) => CastTarget::Named(named),
22082 None => CastTarget::Timestamptz,
22083 },
22084 "interval" => CastTarget::Interval,
22085 "json" => CastTarget::Json,
22086 "jsonb" => CastTarget::Jsonb,
22087 // v7.39 (round 694) — these have dedicated CastTarget
22088 // variants, so they never reached the postfix `[]` handling
22089 // further down and `::regtype[]` was a SYNTAX error at the
22090 // `]`. PG has an array type for every scalar; take the
22091 // suffix here and hand the canonical `<ty>_array` name to
22092 // the engine, the same shape every other array cast uses.
22093 "regtype" if self.peek_postfix_array_brackets() => {
22094 self.advance();
22095 self.advance();
22096 CastTarget::Named(alloc::string::String::from("regtype_array"))
22097 }
22098 "regclass" if self.peek_postfix_array_brackets() => {
22099 self.advance();
22100 self.advance();
22101 CastTarget::Named(alloc::string::String::from("regclass_array"))
22102 }
22103 "regtype" => CastTarget::RegType,
22104 "regclass" => CastTarget::RegClass,
22105 // v7.12.0 — `::tsvector` / `::tsquery`.
22106 // Engine decodes the LHS text via the PG
22107 // external form parser.
22108 // v7.39 (round 352, M8) — MySQL's own cast targets.
22109 // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22110 // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22111 // such type, so they are taken only in that dialect and
22112 // fall through to the "type does not exist" arm otherwise.
22113 "signed" | "unsigned" if self.mysql_dialect => {
22114 if matches!(self.peek(), Token::Ident(k)
22115 if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22116 {
22117 self.advance();
22118 }
22119 CastTarget::Named(s.to_ascii_lowercase())
22120 }
22121 // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22122 // in MySQL: MariaDB answers '123' where the SQL-standard
22123 // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22124 // Truncating a number to its first digit is a wrong answer
22125 // with no error, so the MySQL session gets MySQL's reading.
22126 "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22127 CastTarget::Text
22128 }
22129 "tsvector" => CastTarget::TsVector,
22130 "tsquery" => CastTarget::TsQuery,
22131 // v7.17.0 — `::uuid`. Engine decodes the LHS
22132 // text via `spg_storage::parse_uuid_str`.
22133 "uuid" => CastTarget::Uuid,
22134 // v7.18 — `::bytea`. Engine decodes the LHS
22135 // text via the PG hex form (`'\xdeadbeef'`)
22136 // or escape form (`'\\x05\\x00'`). Closes
22137 // mailrs D-pre #3 reverse-acceptance gap.
22138 "bytea" => CastTarget::Bytea,
22139 // v7.37.5 ship triage — generic typed-cast escape.
22140 // Anything the long-tail PG type ident table knows
22141 // about(network/bit/geometry/multirange/etc.)flows
22142 // through `CastTarget::Named(canonical)`; the engine
22143 // resolves via `column_type_to_data_type` and dispatches
22144 // through the typed `coerce_value` path. Truly
22145 // unrecognised idents still hit the error arm below
22146 // because the engine rejects them.
22147 other => {
22148 // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22149 // `::varchar(255)`, etc. Capture into the canonical
22150 // `name(p,s)` form so `type_name_to_data_type` can
22151 // reconstruct the `DataType::Numeric { precision,
22152 // scale }` (and similar param-carrying types).
22153 let mut name = other.to_string();
22154 // v7.39 (round 281) — `::bit varying(3)` is two
22155 // words; fold the tail in so the typmod reaches the
22156 // type resolver instead of tripping the parser.
22157 if name.eq_ignore_ascii_case("bit")
22158 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22159 {
22160 self.advance();
22161 name = alloc::string::String::from("varbit");
22162 }
22163 // v7.39 (round 613) — `::character varying` is the same
22164 // two-word shape and had no fold, so the `varying` was
22165 // left behind and the cast became a bare `character`,
22166 // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22167 // `a` where PG answers `ab`. Silently, and for a spelling
22168 // pg_dump writes.
22169 if name.eq_ignore_ascii_case("character")
22170 && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22171 {
22172 self.advance();
22173 name = alloc::string::String::from("varchar");
22174 }
22175 if matches!(self.peek(), Token::LParen) {
22176 let mut buf = alloc::string::String::from("(");
22177 let mut depth = 0usize;
22178 loop {
22179 match self.advance() {
22180 Token::LParen => {
22181 depth += 1;
22182 if depth > 1 {
22183 buf.push('(');
22184 }
22185 }
22186 Token::RParen => {
22187 depth -= 1;
22188 if depth == 0 {
22189 buf.push(')');
22190 break;
22191 }
22192 buf.push(')');
22193 }
22194 Token::Comma => buf.push(','),
22195 Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22196 // v7.39 (round 273) — a minus used to fall
22197 // into the catch-all below and vanish, so
22198 // `::numeric(10,-2)` reached the engine as
22199 // the text `numeric(10,2)` and silently
22200 // rounded to two DECIMALS instead of to
22201 // hundreds. A dropped token is not a
22202 // no-op when it carries a sign.
22203 Token::Minus => buf.push('-'),
22204 Token::Eof => break,
22205 _ => {}
22206 }
22207 }
22208 name.push_str(&buf);
22209 }
22210 // Optional postfix `[]` widens to the array form —
22211 // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22212 // The engine's `type_name_to_data_type` recognises
22213 // the canonical `<ty>_array` form.
22214 if matches!(self.peek(), Token::LBracket)
22215 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22216 {
22217 self.advance();
22218 self.advance();
22219 name.push_str("_array");
22220 }
22221 CastTarget::Named(name)
22222 }
22223 },
22224 Token::Interval => CastTarget::Interval,
22225 // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22226 // "char" (oid 18, SPG Char1 — distinct from bare `char`
22227 // = char(1)); other quoted names resolve like idents.
22228 Token::QuotedIdent(q) => {
22229 if q.eq_ignore_ascii_case("char") {
22230 CastTarget::Named("char1".into())
22231 } else {
22232 CastTarget::Named(q.to_ascii_lowercase())
22233 }
22234 }
22235 other => {
22236 return Err(ParseError {
22237 message: format!("expected type ident after `::`, got {other:?}"),
22238 token_pos: self.consumed_pos(),
22239 });
22240 }
22241 };
22242 // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22243 // target to its array sibling. Closed-enum arms (Bool /
22244 // SmallInt / Numeric / Float / Date / …) didn't carry the
22245 // explicit widening that Text / Int / BigInt did, so
22246 // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22247 // error. The widening here mirrors the per-arm Text /
22248 // Int / BigInt logic above + folds the new ζ-A first-class
22249 // types through `CastTarget::Named("<ty>_array")`.
22250 if matches!(self.peek(), Token::LBracket)
22251 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22252 {
22253 let widened = match &target {
22254 CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22255 CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22256 // v7.39 (round 326, V43) — the two temporal types stay
22257 // distinct. Both used to widen to `timestamptz_array`, so
22258 // `::timestamp[]` named the wrong target in its own error
22259 // message and lost the zone-less identity on the way.
22260 CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22261 CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22262 CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22263 CastTarget::Json | CastTarget::Jsonb => {
22264 Some(CastTarget::Named("jsonb_array".to_string()))
22265 }
22266 CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22267 CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22268 CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22269 CastTarget::Named(name) => {
22270 let mut a = name.clone();
22271 a.push_str("_array");
22272 Some(CastTarget::Named(a))
22273 }
22274 // Int / BigInt / Text / Vector / TsVector / TsQuery /
22275 // RegType / RegClass / TextArray / IntArray /
22276 // BigIntArray already finalised — leave as is.
22277 _ => None,
22278 };
22279 if let Some(w) = widened {
22280 self.advance();
22281 self.advance();
22282 return Ok(w);
22283 }
22284 }
22285 Ok(target)
22286 }
22287
22288 fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
22289 loop {
22290 // v7.38 (read01, T9) — composite field access `(expr).field`.
22291 // A bare `a.b` is consumed as a qualified column inside the ident
22292 // atom, so a Dot only survives to this postfix position when the
22293 // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
22294 // `.*` whole-row expansion is not handled here (projection-level).
22295 if matches!(self.peek(), Token::Dot)
22296 && matches!(
22297 self.tokens.get(self.pos + 1),
22298 Some(Token::Ident(_) | Token::QuotedIdent(_))
22299 )
22300 {
22301 self.advance(); // .
22302 let field = match self.advance() {
22303 Token::Ident(s) | Token::QuotedIdent(s) => s,
22304 other => {
22305 return Err(
22306 self.err(format!("expected a field name after '.', got {other:?}"))
22307 );
22308 }
22309 };
22310 expr = Expr::FieldAccess {
22311 base: Box::new(expr),
22312 field,
22313 };
22314 continue;
22315 }
22316 if matches!(self.peek(), Token::DoubleColon) {
22317 self.advance();
22318 // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
22319 // target set to include INTERVAL (reserved Token),
22320 // TIMESTAMPTZ, and PG catalog regtype / regclass.
22321 // mailrs follow-up H3a + H3b.
22322 let target = self.parse_cast_target()?;
22323 expr = Expr::Cast {
22324 expr: Box::new(expr),
22325 target,
22326 };
22327 continue;
22328 }
22329 // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
22330 // returns NULL for out-of-range. Multiple subscripts
22331 // chain: `a[i][j]` parses left-to-right.
22332 if matches!(self.peek(), Token::LBracket) {
22333 self.advance();
22334 // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
22335 // bare index stays a subscript.
22336 let lo = if matches!(self.peek(), Token::Colon) {
22337 None
22338 } else {
22339 Some(self.parse_expr(0)?)
22340 };
22341 if matches!(self.peek(), Token::Colon) {
22342 self.advance();
22343 let hi = if matches!(self.peek(), Token::RBracket) {
22344 None
22345 } else {
22346 Some(Box::new(self.parse_expr(0)?))
22347 };
22348 if !matches!(self.peek(), Token::RBracket) {
22349 return Err(self.err(alloc::format!(
22350 "expected ']' after array slice, got {:?}",
22351 self.peek()
22352 )));
22353 }
22354 self.advance();
22355 expr = Expr::ArraySlice {
22356 target: Box::new(expr),
22357 lo: lo.map(Box::new),
22358 hi,
22359 };
22360 continue;
22361 }
22362 let index = lo.expect("non-colon branch parsed an index");
22363 if !matches!(self.peek(), Token::RBracket) {
22364 return Err(self.err(alloc::format!(
22365 "expected ']' after array index, got {:?}",
22366 self.peek()
22367 )));
22368 }
22369 self.advance();
22370 expr = Expr::ArraySubscript {
22371 target: Box::new(expr),
22372 index: Box::new(index),
22373 };
22374 continue;
22375 }
22376 // `expr AT TIME ZONE zone` — lowers to PG's own function
22377 // form timezone(zone, expr); the scalar implements the
22378 // offset shift (named zones error there — no tzdata).
22379 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
22380 && matches!(self.tokens.get(self.pos + 1),
22381 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
22382 && matches!(self.tokens.get(self.pos + 2),
22383 Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
22384 {
22385 self.advance(); // AT
22386 self.advance(); // TIME
22387 self.advance(); // ZONE
22388 // Zone at comparison precedence so AND/OR stay out.
22389 let zone = self.parse_expr(6)?;
22390 expr = Expr::FunctionCall {
22391 name: "timezone".to_string(),
22392 args: alloc::vec![zone, expr],
22393 };
22394 continue;
22395 }
22396 // `expr COLLATE "name"` — SPG's single text ordering IS
22397 // byte order, i.e. the C collation. The byte-order
22398 // spellings absorb as no-ops; a locale collation would
22399 // silently sort differently from PG, so it errors
22400 // honestly instead.
22401 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
22402 self.advance();
22403 let mut cname = match self.advance() {
22404 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22405 other => {
22406 return Err(self.err(alloc::format!(
22407 "expected collation name after COLLATE, got {other:?}"
22408 )));
22409 }
22410 };
22411 // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
22412 // is how `pg_dump` writes the default one:
22413 // `… COLLATE pg_catalog.default`. Reading a single token
22414 // left the SCHEMA as the name, so the clause was refused
22415 // as an unsupported locale collation and no dump ran.
22416 if matches!(self.peek(), Token::Dot) {
22417 self.advance();
22418 cname = match self.advance() {
22419 Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
22420 // `default` lexes as a KEYWORD, and it is the name
22421 // pg_dump writes — the same trap round 535 hit with
22422 // TABLE / INDEX / FULL.
22423 Token::Default => alloc::string::String::from("default"),
22424 other => {
22425 return Err(self.err(alloc::format!(
22426 "expected collation name after COLLATE, got {other:?}"
22427 )));
22428 }
22429 };
22430 }
22431 let lc = cname.to_ascii_lowercase();
22432 // v7.39 (round 371, M4 P4b) — a per-expression MySQL
22433 // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
22434 // family / `binary`) forces byte-wise, which is exactly
22435 // what `BINARY expr` does — lower onto that so every fold
22436 // site (comparison, LIKE, ORDER BY) suppresses via
22437 // `is_binary_coerced`. A `_ci` family override folds, and
22438 // under the MySQL dialect the default already folds, so it
22439 // absorbs as a no-op; likewise the C / byte-order spellings.
22440 if self.mysql_dialect && (lc.ends_with("_bin") || lc == "binary") {
22441 expr = Expr::Cast {
22442 expr: alloc::boxed::Box::new(expr),
22443 target: CastTarget::Named("binary".to_string()),
22444 };
22445 continue;
22446 }
22447 let mysql_ci = self.mysql_dialect
22448 && (lc.ends_with("_ci")
22449 || matches!(lc.as_str(), "case_insensitive" | "nocase"));
22450 // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
22451 // goes to the lowering channel, the byte-order spellings
22452 // included. Round 691 recorded only the names the old
22453 // allow-list rejected, which left `ORDER BY a COLLATE "C"`
22454 // absorbed as a no-op — and once a column could declare a
22455 // collation, absorbing the clause meant the COLUMN's
22456 // collation won where the query had asked for bytes.
22457 if self.in_order_by_key && !mysql_ci {
22458 self.order_key_collation = Some(cname);
22459 continue;
22460 }
22461 if !matches!(
22462 lc.as_str(),
22463 "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
22464 ) && !mysql_ci
22465 {
22466 return Err(self.err(alloc::format!(
22467 "COLLATE {cname:?}: SPG orders text by bytes (the C \
22468 collation); locale collations are not supported yet — \
22469 use COLLATE \"C\" or drop the clause"
22470 )));
22471 }
22472 continue;
22473 }
22474 return Ok(expr);
22475 }
22476 }
22477
22478 /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
22479 /// the first token that is not one. Schema qualifiers collapse to the
22480 /// last part, which is what every other name path here does (SPG is
22481 /// single-schema).
22482 fn take_comma_separated_names(&mut self) -> Vec<String> {
22483 let mut out = Vec::new();
22484 while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
22485 self.advance();
22486 let mut last = n;
22487 while matches!(self.peek(), Token::Dot) {
22488 self.advance();
22489 if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
22490 last = t;
22491 }
22492 }
22493 out.push(last);
22494 if matches!(self.peek(), Token::Comma) {
22495 self.advance();
22496 } else {
22497 break;
22498 }
22499 }
22500 out
22501 }
22502
22503 /// v7.39 (round 694) — is the next token pair a postfix `[]`?
22504 ///
22505 /// The general cast-target path tests this inline; the types with their
22506 /// own `CastTarget` variant need it as a guard on their match arm,
22507 /// which is what this exists for.
22508 fn peek_postfix_array_brackets(&self) -> bool {
22509 matches!(self.peek(), Token::LBracket)
22510 && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22511 }
22512
22513 /// Parse the operator tail after a `(a, b, …)` row constructor
22514 /// and expand at parse time. `=` is the conjunction of element
22515 /// equalities; `<>` its negation; the order operators expand
22516 /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
22517 /// equalities. Anything else (a bare row value, a subquery
22518 /// RHS) errors honestly — SPG has no composite runtime value.
22519 fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
22520 fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
22521 let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
22522 lhs: Box::new(l.clone()),
22523 op: BinOp::Eq,
22524 rhs: Box::new(r.clone()),
22525 });
22526 let first = it.next().expect("row has at least two elements");
22527 it.fold(first, |acc, e| Expr::Binary {
22528 lhs: Box::new(acc),
22529 op: BinOp::And,
22530 rhs: Box::new(e),
22531 })
22532 }
22533 // Lexicographic (a,b) OP (c,d):
22534 // a STRICT c OR (a = c AND (b OP d)) — recursing right.
22535 fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
22536 if lhs.len() == 1 {
22537 return Expr::Binary {
22538 lhs: Box::new(lhs[0].clone()),
22539 op: last,
22540 rhs: Box::new(rhs[0].clone()),
22541 };
22542 }
22543 let head_strict = Expr::Binary {
22544 lhs: Box::new(lhs[0].clone()),
22545 op: strict,
22546 rhs: Box::new(rhs[0].clone()),
22547 };
22548 let head_eq = Expr::Binary {
22549 lhs: Box::new(lhs[0].clone()),
22550 op: BinOp::Eq,
22551 rhs: Box::new(rhs[0].clone()),
22552 };
22553 Expr::Binary {
22554 lhs: Box::new(head_strict),
22555 op: BinOp::Or,
22556 rhs: Box::new(Expr::Binary {
22557 lhs: Box::new(head_eq),
22558 op: BinOp::And,
22559 rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
22560 }),
22561 }
22562 }
22563 let negated_in = if matches!(self.peek(), Token::Not)
22564 && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
22565 {
22566 self.advance();
22567 true
22568 } else {
22569 false
22570 };
22571 if matches!(self.peek(), Token::In) {
22572 self.advance();
22573 if !matches!(self.peek(), Token::LParen) {
22574 return Err(self.err(alloc::format!(
22575 "expected '(' after row IN, got {:?}",
22576 self.peek()
22577 )));
22578 }
22579 self.advance();
22580 // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
22581 // not a list of literal rows. Row-vs-list decomposes to
22582 // OR-of-AND above, but the subquery's rows are only known at
22583 // runtime, so keep it as a RowInSubquery node.
22584 if matches!(self.peek(), Token::Select) {
22585 let inner = self.parse_select_stmt()?;
22586 if !matches!(self.peek(), Token::RParen) {
22587 return Err(self.err(alloc::format!(
22588 "expected ')' after row IN-subquery, got {:?}",
22589 self.peek()
22590 )));
22591 }
22592 self.advance();
22593 let Statement::Select(s) = inner else {
22594 unreachable!("parse_select_stmt always returns Statement::Select")
22595 };
22596 return Ok(Expr::RowInSubquery {
22597 row,
22598 subquery: Box::new(s),
22599 negated: negated_in,
22600 });
22601 }
22602 let mut alternatives: Vec<Expr> = Vec::new();
22603 loop {
22604 // Optional ROW keyword before the paren row.
22605 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22606 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22607 {
22608 self.advance();
22609 }
22610 if !matches!(self.peek(), Token::LParen) {
22611 return Err(self.err(alloc::format!(
22612 "expected '(' to open a row inside IN, got {:?}",
22613 self.peek()
22614 )));
22615 }
22616 self.advance();
22617 let mut rhs = alloc::vec![self.parse_expr(0)?];
22618 while matches!(self.peek(), Token::Comma) {
22619 self.advance();
22620 rhs.push(self.parse_expr(0)?);
22621 }
22622 if !matches!(self.peek(), Token::RParen) {
22623 return Err(self.err(alloc::format!(
22624 "expected ')' after row inside IN, got {:?}",
22625 self.peek()
22626 )));
22627 }
22628 self.advance();
22629 if rhs.len() != row.len() {
22630 return Err(self.err(alloc::format!(
22631 "row IN arity mismatch: left has {}, right has {}",
22632 row.len(),
22633 rhs.len()
22634 )));
22635 }
22636 alternatives.push(row_eq(&row, &rhs));
22637 if matches!(self.peek(), Token::Comma) {
22638 self.advance();
22639 continue;
22640 }
22641 break;
22642 }
22643 if !matches!(self.peek(), Token::RParen) {
22644 return Err(self.err(alloc::format!(
22645 "expected ')' to close row IN list, got {:?}",
22646 self.peek()
22647 )));
22648 }
22649 self.advance();
22650 let mut it = alternatives.into_iter();
22651 let first = it.next().expect("IN list has at least one row");
22652 let combined = it.fold(first, |acc, e| Expr::Binary {
22653 lhs: Box::new(acc),
22654 op: BinOp::Or,
22655 rhs: Box::new(e),
22656 });
22657 return Ok(maybe_not(combined, negated_in));
22658 }
22659 // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
22660 // two periods share at least one time point. Each pair is
22661 // normalised with least/greatest (PG accepts the endpoints
22662 // in either order), then lowered to the standard
22663 // `start1 < end2 AND start2 < end1` form.
22664 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
22665 if row.len() != 2 {
22666 return Err(self.err(alloc::format!(
22667 "OVERLAPS needs (start, end) pairs; left side has {} elements",
22668 row.len()
22669 )));
22670 }
22671 self.advance();
22672 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22673 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22674 {
22675 self.advance();
22676 }
22677 if !matches!(self.peek(), Token::LParen) {
22678 return Err(self.err(alloc::format!(
22679 "expected '(' after OVERLAPS, got {:?}",
22680 self.peek()
22681 )));
22682 }
22683 self.advance();
22684 let r0 = self.parse_expr(0)?;
22685 if !matches!(self.peek(), Token::Comma) {
22686 return Err(self.err(alloc::format!(
22687 "OVERLAPS needs (start, end) on the right, got {:?}",
22688 self.peek()
22689 )));
22690 }
22691 self.advance();
22692 let r1 = self.parse_expr(0)?;
22693 if !matches!(self.peek(), Token::RParen) {
22694 return Err(self.err(alloc::format!(
22695 "expected ')' after OVERLAPS pair, got {:?}",
22696 self.peek()
22697 )));
22698 }
22699 self.advance();
22700 let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
22701 name: String::from(name),
22702 args: alloc::vec![a.clone(), b.clone()],
22703 };
22704 let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
22705 lhs: Box::new(lhs),
22706 op: BinOp::Lt,
22707 rhs: Box::new(rhs),
22708 };
22709 return Ok(Expr::Binary {
22710 lhs: Box::new(lt(
22711 pair_fn("least", &row[0], &row[1]),
22712 pair_fn("greatest", &r0, &r1),
22713 )),
22714 op: BinOp::And,
22715 rhs: Box::new(lt(
22716 pair_fn("least", &r0, &r1),
22717 pair_fn("greatest", &row[0], &row[1]),
22718 )),
22719 });
22720 }
22721 // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
22722 // PG, `IS NULL` is true only when EVERY field is NULL, and
22723 // `IS NOT NULL` is true only when every field is non-NULL — the
22724 // latter is NOT the negation of the former (a mixed row is
22725 // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
22726 // which reproduces exactly that all-fields semantics.
22727 if matches!(self.peek(), Token::Is) {
22728 self.advance();
22729 let negated = if matches!(self.peek(), Token::Not) {
22730 self.advance();
22731 true
22732 } else {
22733 false
22734 };
22735 if !matches!(self.peek(), Token::Null) {
22736 return Err(self.err(alloc::format!(
22737 "expected NULL after row IS [NOT], got {:?}",
22738 self.peek()
22739 )));
22740 }
22741 self.advance();
22742 let mut it = row.iter().map(|e| Expr::IsNull {
22743 expr: Box::new(e.clone()),
22744 negated,
22745 });
22746 let first = it.next().expect("row has at least two elements");
22747 return Ok(it.fold(first, |acc, e| Expr::Binary {
22748 lhs: Box::new(acc),
22749 op: BinOp::And,
22750 rhs: Box::new(e),
22751 }));
22752 }
22753 let op = match self.peek() {
22754 Token::Eq => BinOp::Eq,
22755 Token::NotEq => BinOp::NotEq,
22756 Token::Lt => BinOp::Lt,
22757 Token::LtEq => BinOp::LtEq,
22758 Token::Gt => BinOp::Gt,
22759 Token::GtEq => BinOp::GtEq,
22760 // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
22761 // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
22762 // constructor value, identical to the `ROW(a, b, …)` keyword form:
22763 // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
22764 // (`::text`, `.field`) applies at the caller just as it does for the
22765 // ROW(...) node. All the comparison / predicate forms returned above.
22766 _ => {
22767 return Ok(Expr::FunctionCall {
22768 name: String::from("row"),
22769 args: row,
22770 });
22771 }
22772 };
22773 self.advance();
22774 // Optional ROW keyword before the paren row.
22775 if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
22776 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
22777 {
22778 self.advance();
22779 }
22780 if !matches!(self.peek(), Token::LParen) {
22781 return Err(self.err(alloc::format!(
22782 "expected '(' to open the right-hand row, got {:?}",
22783 self.peek()
22784 )));
22785 }
22786 self.advance();
22787 // `(a, b) <op> (SELECT x, y)` — compare against a single-row
22788 // subquery. Kept as a node (the subquery's row is a runtime value);
22789 // the literal-RHS form below still decomposes at parse time.
22790 if matches!(self.peek(), Token::Select) {
22791 let inner = self.parse_select_stmt()?;
22792 if !matches!(self.peek(), Token::RParen) {
22793 return Err(self.err(alloc::format!(
22794 "expected ')' after row comparison subquery, got {:?}",
22795 self.peek()
22796 )));
22797 }
22798 self.advance();
22799 let Statement::Select(s) = inner else {
22800 unreachable!("parse_select_stmt always returns Statement::Select")
22801 };
22802 return Ok(Expr::RowCmpSubquery {
22803 row,
22804 op,
22805 subquery: Box::new(s),
22806 });
22807 }
22808 let mut rhs = alloc::vec![self.parse_expr(0)?];
22809 while matches!(self.peek(), Token::Comma) {
22810 self.advance();
22811 rhs.push(self.parse_expr(0)?);
22812 }
22813 if !matches!(self.peek(), Token::RParen) {
22814 return Err(self.err(alloc::format!(
22815 "expected ')' after right-hand row, got {:?}",
22816 self.peek()
22817 )));
22818 }
22819 self.advance();
22820 if rhs.len() != row.len() {
22821 // v7.39 (round 239) — PG's wording (42601).
22822 return Err(self.err("unequal number of entries in row expressions".to_string()));
22823 }
22824 Ok(match op {
22825 BinOp::Eq => row_eq(&row, &rhs),
22826 BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
22827 BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
22828 BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
22829 BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
22830 BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
22831 _ => unreachable!("op restricted above"),
22832 })
22833 }
22834
22835 /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
22836 /// escape character becomes the matcher's default backslash:
22837 /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
22838 /// → the char itself, and any pre-existing backslash escapes
22839 /// itself so it stays literal. Both operands must be string
22840 /// literals — a runtime pattern would need matcher support.
22841 fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
22842 let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
22843 (&pattern, &esc)
22844 else {
22845 return Err(
22846 "LIKE ... ESCAPE requires string-literal pattern and escape \
22847 (runtime escape characters are not supported yet)"
22848 .into(),
22849 );
22850 };
22851 // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
22852 // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
22853 // multi-character escape is an error.
22854 let esc_ch: Option<char> = {
22855 let mut ch_iter = e.chars();
22856 match (ch_iter.next(), ch_iter.next()) {
22857 (Some(c), None) => Some(c),
22858 (None, _) => None,
22859 (Some(_), Some(_)) => {
22860 return Err(alloc::format!(
22861 "ESCAPE must be a single character, got {e:?}"
22862 ));
22863 }
22864 }
22865 };
22866 let mut out = String::with_capacity(p.len() + 4);
22867 let mut chars = p.chars();
22868 while let Some(c) = chars.next() {
22869 if Some(c) == esc_ch {
22870 match chars.next() {
22871 // Escaped wildcard / escaped escape → keep the
22872 // next char literal via backslash.
22873 Some(next) => {
22874 out.push('\\');
22875 out.push(next);
22876 }
22877 None => {
22878 return Err("LIKE pattern ends with the escape character".into());
22879 }
22880 }
22881 } else if c == '\\' && esc_ch != Some('\\') {
22882 // A raw backslash is literal under a custom (or absent) escape
22883 // — escape it for the backslash-based matcher.
22884 out.push('\\');
22885 out.push('\\');
22886 } else {
22887 out.push(c);
22888 }
22889 }
22890 Ok(Expr::Literal(Literal::String(out)))
22891 }
22892
22893 /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
22894 /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
22895 /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
22896 /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
22897 /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
22898 /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
22899 /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
22900 /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
22901 /// array expression errors honestly rather than silently mismatching.
22902 fn try_like_any_all(
22903 &mut self,
22904 base: &Expr,
22905 negated: bool,
22906 case_insensitive: bool,
22907 ) -> Result<Option<Expr>, ParseError> {
22908 let is_any = match self.peek() {
22909 Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
22910 Token::Ident(s)
22911 if s.eq_ignore_ascii_case("any")
22912 && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22913 {
22914 true
22915 }
22916 _ => return Ok(None),
22917 };
22918 self.advance(); // ANY / ALL
22919 self.advance(); // '('
22920 let arr = self.parse_expr(0)?;
22921 if !matches!(self.peek(), Token::RParen) {
22922 return Err(self.err(format!(
22923 "expected ')' after LIKE {} argument, got {:?}",
22924 if is_any { "ANY" } else { "ALL" },
22925 self.peek()
22926 )));
22927 }
22928 self.advance(); // ')'
22929 let Expr::Array(items) = arr else {
22930 return Err(self.err(
22931 "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
22932 ));
22933 };
22934 let mut clauses = items.into_iter().map(|p| Expr::Like {
22935 expr: Box::new(base.clone()),
22936 pattern: Box::new(p),
22937 negated,
22938 case_insensitive,
22939 });
22940 let Some(first) = clauses.next() else {
22941 // ANY(empty) = FALSE, ALL(empty) = TRUE.
22942 return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
22943 };
22944 let op = if is_any { BinOp::Or } else { BinOp::And };
22945 let combined = clauses.fold(first, |acc, c| Expr::Binary {
22946 lhs: Box::new(acc),
22947 op,
22948 rhs: Box::new(c),
22949 });
22950 Ok(Some(combined))
22951 }
22952
22953 /// `x BETWEEN low AND high` → `(x >= low) AND (x <= high)`, wrapped in
22954 /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
22955 /// `AND` is not swallowed.
22956 fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
22957 self.advance(); // BETWEEN
22958 // SYMMETRIC — the bounds may arrive in either order; both
22959 // orientations OR together. ASYMMETRIC is the default and
22960 // absorbs as noise.
22961 let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
22962 {
22963 self.advance();
22964 true
22965 } else {
22966 if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
22967 self.advance();
22968 }
22969 false
22970 };
22971 let low = self.parse_expr(6)?;
22972 if !matches!(self.peek(), Token::And) {
22973 return Err(self.err(format!(
22974 "expected AND after BETWEEN low bound, got {:?}",
22975 self.peek()
22976 )));
22977 }
22978 self.advance();
22979 let high = self.parse_expr(6)?;
22980 let target = Box::new(expr);
22981 let range = |lo: Expr, hi: Expr| Expr::Binary {
22982 lhs: Box::new(Expr::Binary {
22983 lhs: target.clone(),
22984 op: BinOp::GtEq,
22985 rhs: Box::new(lo),
22986 }),
22987 op: BinOp::And,
22988 rhs: Box::new(Expr::Binary {
22989 lhs: target.clone(),
22990 op: BinOp::LtEq,
22991 rhs: Box::new(hi),
22992 }),
22993 };
22994 let combined = if symmetric {
22995 Expr::Binary {
22996 lhs: Box::new(range(low.clone(), high.clone())),
22997 op: BinOp::Or,
22998 rhs: Box::new(range(high, low)),
22999 }
23000 } else {
23001 range(low, high)
23002 };
23003 Ok(maybe_not(combined, negated))
23004 }
23005
23006 /// `x IN (a, b, c)` → chained OR of equalities. Empty list collapses
23007 /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23008 /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23009 /// Caller already consumed the leading `WITH` ident.
23010 /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23011 /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23012 /// self-reference that appears more than once in a single term.
23013 fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23014 use crate::ast::{CteBody, SelectStatement};
23015 if !cte.recursive {
23016 return Ok(());
23017 }
23018 let CteBody::Select(body) = &cte.body else {
23019 return Ok(());
23020 };
23021 // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23022 // check the anchor and every peer term.
23023 let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23024 let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23025 if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23026 return Err(self.err(String::from(
23027 "ORDER BY in a recursive query is not implemented",
23028 )));
23029 }
23030 if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23031 return Err(self.err(String::from(
23032 "LIMIT in a recursive query is not implemented",
23033 )));
23034 }
23035 let self_refs = |s: &SelectStatement| -> usize {
23036 let Some(from) = &s.from else {
23037 return 0;
23038 };
23039 let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23040 for j in &from.joins {
23041 if j.table.name.eq_ignore_ascii_case(&cte.name) {
23042 n += 1;
23043 }
23044 }
23045 n
23046 };
23047 if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23048 return Err(self.err(alloc::format!(
23049 "recursive reference to query \"{}\" must not appear more than once",
23050 cte.name
23051 )));
23052 }
23053 // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23054 // apply only when the body actually references itself (a non-self-
23055 // referencing CTE under WITH RECURSIVE may use any set-op shape).
23056 let anchor_refs = self_refs(body);
23057 let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23058 if anchor_refs > 0 || union_refs {
23059 // Shape: the top level must be UNION [ALL] arms only. A self-ref
23060 // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23061 // "does not have the form" error — SPG used to compute a value.
23062 if body.unions.is_empty()
23063 || body.unions.iter().any(|(k, _)| {
23064 !matches!(
23065 k,
23066 crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23067 )
23068 })
23069 {
23070 return Err(self.err(alloc::format!(
23071 "recursive query \"{}\" does not have the form non-recursive-term \
23072 UNION [ALL] recursive-term",
23073 cte.name
23074 )));
23075 }
23076 if anchor_refs > 0 {
23077 return Err(self.err(alloc::format!(
23078 "recursive reference to query \"{}\" must not appear within its non-recursive term",
23079 cte.name
23080 )));
23081 }
23082 }
23083 let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23084 for (_, u) in &body.unions {
23085 if self_refs(u) == 0 {
23086 continue;
23087 }
23088 // The self-reference must not sit on the nullable side of an outer
23089 // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23090 if let Some(from) = &u.from {
23091 for (i, j) in from.joins.iter().enumerate() {
23092 let left_has_self = is_self(&from.primary)
23093 || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23094 let violated = match j.kind {
23095 crate::ast::JoinKind::Left => is_self(&j.table),
23096 crate::ast::JoinKind::Right => left_has_self,
23097 crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23098 _ => false,
23099 };
23100 if violated {
23101 return Err(self.err(alloc::format!(
23102 "recursive reference to query \"{}\" must not appear within an outer join",
23103 cte.name
23104 )));
23105 }
23106 }
23107 }
23108 // No aggregates at the top level of the recursive term (SPG used
23109 // to run them and surface a misleading downstream error).
23110 let mut items_and_having: Vec<&Expr> = Vec::new();
23111 for it in &u.items {
23112 if let crate::ast::SelectItem::Expr { expr, .. } = it {
23113 items_and_having.push(expr);
23114 }
23115 }
23116 if let Some(h) = &u.having {
23117 items_and_having.push(h);
23118 }
23119 for e in items_and_having {
23120 if expr_has_toplevel_aggregate(e) {
23121 return Err(self.err(String::from(
23122 "aggregate functions are not allowed in a recursive query's recursive term",
23123 )));
23124 }
23125 }
23126 }
23127 // A self-reference inside a sublink expression (EXISTS / IN / scalar
23128 // subquery) anywhere in the body is rejected; a plain FROM derived
23129 // table is legal in PG and untouched here.
23130 let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23131 all_terms.extend(body.unions.iter().map(|(_, u)| u));
23132 for term in all_terms {
23133 if select_has_self_ref_in_sublink(term, &cte.name) {
23134 return Err(self.err(alloc::format!(
23135 "recursive reference to query \"{}\" must not appear within a subquery",
23136 cte.name
23137 )));
23138 }
23139 }
23140 Ok(())
23141 }
23142
23143 /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23144 /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23145 /// right after parse so the engine sees a plain recursive CTE with the
23146 /// tracking columns already projected. DEPTH FIRST and CYCLE are
23147 /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23148 /// text-rendered rows can't provide, and errors honestly.
23149 fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23150 use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23151 if cte.search.is_none() && cte.cycle.is_none() {
23152 return Ok(());
23153 }
23154 let cte_name = cte.name.clone();
23155 let col_names = cte.column_overrides.clone();
23156 if col_names.is_empty() {
23157 return Err(
23158 self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23159 );
23160 }
23161 let search = cte.search.take();
23162 let cycle = cte.cycle.take();
23163 let mut extra_cols: Vec<String> = Vec::new();
23164 let col_ref = |name: &str| {
23165 Expr::Column(ColumnName {
23166 qualifier: Some(cte_name.clone()),
23167 name: name.to_string(),
23168 })
23169 };
23170 // Position of a SEARCH/CYCLE column within the CTE's column list.
23171 let pos_of = |name: &str| -> Result<usize, ParseError> {
23172 col_names
23173 .iter()
23174 .position(|c| c.eq_ignore_ascii_case(name))
23175 .ok_or_else(|| {
23176 self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23177 })
23178 };
23179 let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23180 let mut args = Vec::with_capacity(positions.len());
23181 for &p in positions {
23182 match items.get(p) {
23183 Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23184 _ => {
23185 return Err(self.err(
23186 "SEARCH/CYCLE column maps to a non-expression select item".into(),
23187 ));
23188 }
23189 }
23190 }
23191 Ok(Expr::FunctionCall {
23192 name: "row".into(),
23193 args,
23194 })
23195 };
23196 let CteBody::Select(body) = &mut cte.body else {
23197 return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23198 };
23199 if body.unions.is_empty() {
23200 return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23201 }
23202 let rec = body.unions.len() - 1; // recursive term = last UNION peer
23203
23204 if let Some(srch) = search {
23205 // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23206 // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23207 // no typed `record[]`, but element-wise array ORDER BY is correct
23208 // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23209 // exactly onto a typed array: DEPTH is the root→node path
23210 // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23211 // orders numerically (multi-digit keys included), matching PG.
23212 //
23213 // A multi-column BY would need a record[] to keep the per-node key
23214 // tuple orderable, which SPG can't express — error honestly there
23215 // rather than mis-order.
23216 if srch.by_columns.len() != 1 {
23217 return Err(self.err(
23218 "SEARCH … BY with multiple columns needs typed record[] ordering \
23219 SPG doesn't have yet; a single BY column is supported"
23220 .into(),
23221 ));
23222 }
23223 let key_pos = pos_of(&srch.by_columns[0])?;
23224 let base_key = match body.items.get(key_pos) {
23225 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23226 _ => {
23227 return Err(
23228 self.err("SEARCH BY column maps to a non-expression select item".into())
23229 );
23230 }
23231 };
23232 let rec_key = match body.unions[rec].1.items.get(key_pos) {
23233 Some(SelectItem::Expr { expr, .. }) => expr.clone(),
23234 _ => {
23235 return Err(
23236 self.err("SEARCH BY column maps to a non-expression select item".into())
23237 );
23238 }
23239 };
23240 if srch.depth_first {
23241 // base: ARRAY[key]; rec: array_append(cte.set, key).
23242 body.items.push(SelectItem::Expr {
23243 expr: Expr::Array(alloc::vec![base_key]),
23244 alias: Some(srch.set_column.clone()),
23245 });
23246 body.unions[rec].1.items.push(SelectItem::Expr {
23247 expr: Expr::FunctionCall {
23248 name: "array_append".into(),
23249 args: alloc::vec![col_ref(&srch.set_column), rec_key],
23250 },
23251 alias: Some(srch.set_column.clone()),
23252 });
23253 } else {
23254 // BREADTH: [depth, key]; depth starts at 0 and increments. The
23255 // leading depth element dominates the element-wise comparison,
23256 // so shallower rows sort first, then by key — PG's (depth, key).
23257 body.items.push(SelectItem::Expr {
23258 expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
23259 alias: Some(srch.set_column.clone()),
23260 });
23261 // rec depth = cte.set[1] + 1.
23262 let parent_depth = Expr::ArraySubscript {
23263 target: Box::new(col_ref(&srch.set_column)),
23264 index: Box::new(Expr::Literal(Literal::Integer(1))),
23265 };
23266 body.unions[rec].1.items.push(SelectItem::Expr {
23267 expr: Expr::Array(alloc::vec![
23268 Expr::Binary {
23269 lhs: Box::new(parent_depth),
23270 op: BinOp::Add,
23271 rhs: Box::new(Expr::Literal(Literal::Integer(1))),
23272 },
23273 rec_key,
23274 ]),
23275 alias: Some(srch.set_column.clone()),
23276 });
23277 }
23278 extra_cols.push(srch.set_column);
23279 }
23280
23281 if let Some(cyc) = cycle {
23282 let positions: Vec<usize> = cyc
23283 .columns
23284 .iter()
23285 .map(|c| pos_of(c))
23286 .collect::<Result<_, _>>()?;
23287 // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
23288 // cast it to text for the cycle path: membership only needs equality,
23289 // and the record text form gives SPG a TextArray path (SPG has no
23290 // typed record[] array). Cycle detection is unaffected.
23291 let base_row = Expr::Cast {
23292 expr: Box::new(row_of(&body.items, &positions)?),
23293 target: CastTarget::Text,
23294 };
23295 let rec_row = Expr::Cast {
23296 expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
23297 target: CastTarget::Text,
23298 };
23299 let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
23300 let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
23301 // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
23302 body.items.push(SelectItem::Expr {
23303 expr: Expr::Literal(dflt.clone()),
23304 alias: Some(cyc.mark_column.clone()),
23305 });
23306 body.items.push(SelectItem::Expr {
23307 expr: Expr::Array(alloc::vec![base_row]),
23308 alias: Some(cyc.path_column.clone()),
23309 });
23310 // rec mark: ROW(cols) already in the path → cycle.
23311 let hit = Expr::AnyAll {
23312 expr: Box::new(rec_row.clone()),
23313 op: BinOp::Eq,
23314 array: Box::new(col_ref(&cyc.path_column)),
23315 is_any: true,
23316 };
23317 let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
23318 Expr::Case {
23319 operand: None,
23320 branches: alloc::vec![(hit, Expr::Literal(mark))],
23321 else_branch: Some(Box::new(Expr::Literal(dflt))),
23322 }
23323 } else {
23324 hit
23325 };
23326 body.unions[rec].1.items.push(SelectItem::Expr {
23327 expr: mark_expr,
23328 alias: Some(cyc.mark_column.clone()),
23329 });
23330 // rec path: array_append(cte.path, ROW(cols)).
23331 body.unions[rec].1.items.push(SelectItem::Expr {
23332 expr: Expr::FunctionCall {
23333 name: "array_append".into(),
23334 args: alloc::vec![col_ref(&cyc.path_column), rec_row],
23335 },
23336 alias: Some(cyc.path_column.clone()),
23337 });
23338 // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
23339 let stop = Expr::Unary {
23340 op: UnOp::Not,
23341 expr: Box::new(col_ref(&cyc.mark_column)),
23342 };
23343 let w = &mut body.unions[rec].1.where_;
23344 *w = Some(match w.take() {
23345 Some(prev) => Expr::Binary {
23346 lhs: Box::new(prev),
23347 op: BinOp::And,
23348 rhs: Box::new(stop),
23349 },
23350 None => stop,
23351 });
23352 extra_cols.push(cyc.mark_column);
23353 extra_cols.push(cyc.path_column);
23354 }
23355 cte.column_overrides.extend(extra_cols);
23356 Ok(())
23357 }
23358
23359 /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
23360 /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
23361 fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
23362 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
23363 return Ok(None);
23364 }
23365 self.advance(); // SEARCH
23366 let depth_first = match self.peek() {
23367 Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
23368 Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
23369 other => {
23370 return Err(self.err(format!(
23371 "expected DEPTH or BREADTH after SEARCH, got {other:?}"
23372 )));
23373 }
23374 };
23375 self.advance();
23376 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
23377 return Err(self.err(format!(
23378 "expected FIRST after SEARCH mode, got {:?}",
23379 self.peek()
23380 )));
23381 }
23382 self.advance();
23383 if !self.peek_is_by() {
23384 return Err(self.err(format!(
23385 "expected BY after SEARCH … FIRST, got {:?}",
23386 self.peek()
23387 )));
23388 }
23389 self.advance();
23390 let mut by_columns = alloc::vec![self.expect_ident_like()?];
23391 while matches!(self.peek(), Token::Comma) {
23392 self.advance();
23393 by_columns.push(self.expect_ident_like()?);
23394 }
23395 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23396 return Err(self.err(format!(
23397 "expected SET in SEARCH clause, got {:?}",
23398 self.peek()
23399 )));
23400 }
23401 self.advance();
23402 let set_column = self.expect_ident_like()?;
23403 Ok(Some(crate::ast::SearchClause {
23404 depth_first,
23405 by_columns,
23406 set_column,
23407 }))
23408 }
23409
23410 /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
23411 /// USING pathcol`. Returns None when the next token isn't CYCLE.
23412 fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
23413 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
23414 return Ok(None);
23415 }
23416 self.advance(); // CYCLE
23417 let mut columns = alloc::vec![self.expect_ident_like()?];
23418 while matches!(self.peek(), Token::Comma) {
23419 self.advance();
23420 columns.push(self.expect_ident_like()?);
23421 }
23422 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
23423 return Err(self.err(format!(
23424 "expected SET in CYCLE clause, got {:?}",
23425 self.peek()
23426 )));
23427 }
23428 self.advance();
23429 let mark_column = self.expect_ident_like()?;
23430 let mut mark_value = None;
23431 let mut default_value = None;
23432 if matches!(self.peek(), Token::To) {
23433 self.advance();
23434 mark_value = Some(self.parse_cycle_literal()?);
23435 if !matches!(self.peek(), Token::Default) {
23436 return Err(self.err(format!(
23437 "expected DEFAULT after CYCLE … TO, got {:?}",
23438 self.peek()
23439 )));
23440 }
23441 self.advance();
23442 default_value = Some(self.parse_cycle_literal()?);
23443 }
23444 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
23445 return Err(self.err(format!(
23446 "expected USING in CYCLE clause, got {:?}",
23447 self.peek()
23448 )));
23449 }
23450 self.advance();
23451 let path_column = self.expect_ident_like()?;
23452 Ok(Some(crate::ast::CycleClause {
23453 columns,
23454 mark_column,
23455 mark_value,
23456 default_value,
23457 path_column,
23458 }))
23459 }
23460
23461 /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
23462 /// literal (string / bool / number) in PG.
23463 fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
23464 match self.parse_expr(0)? {
23465 Expr::Literal(l) => Ok(l),
23466 other => Err(self.err(format!(
23467 "CYCLE mark/default value must be a literal, got {other:?}"
23468 ))),
23469 }
23470 }
23471
23472 fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
23473 // v4.22: WITH RECURSIVE — optional keyword right after WITH.
23474 // Comes through as an identifier; consume it if present and
23475 // mark every CTE in the clause as recursive (PG semantics —
23476 // the flag is per-WITH, not per-CTE).
23477 let mut recursive = false;
23478 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
23479 && s.eq_ignore_ascii_case("recursive")
23480 {
23481 self.advance();
23482 recursive = true;
23483 }
23484 let mut ctes = Vec::new();
23485 loop {
23486 let name = self.expect_ident_like()?;
23487 // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
23488 // PG uses these to rename the body's output columns; we
23489 // do the same below by overriding `columns[i].name`.
23490 let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
23491 self.advance();
23492 let mut names = Vec::new();
23493 loop {
23494 names.push(self.expect_ident_like()?);
23495 if matches!(self.peek(), Token::Comma) {
23496 self.advance();
23497 continue;
23498 }
23499 break;
23500 }
23501 if !matches!(self.peek(), Token::RParen) {
23502 return Err(self.err(format!(
23503 "expected ')' to close CTE column list, got {:?}",
23504 self.peek()
23505 )));
23506 }
23507 self.advance();
23508 names
23509 } else {
23510 Vec::new()
23511 };
23512 // AS is a reserved Token::As (used by SELECT-item / FROM
23513 // aliasing) — handle it specially rather than as a bare
23514 // ident.
23515 if !matches!(self.peek(), Token::As) {
23516 return Err(self.err(format!(
23517 "expected AS after CTE name {name:?}, got {:?}",
23518 self.peek()
23519 )));
23520 }
23521 self.advance();
23522 // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
23523 // MATERIALIZED` optimizer hints. SPG materialises every
23524 // CTE, so both spellings are accepted and absorbed.
23525 if matches!(self.peek(), Token::Not) {
23526 self.advance(); // NOT
23527 if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23528 if s.eq_ignore_ascii_case("materialized"))
23529 {
23530 self.advance();
23531 } else {
23532 return Err(self.err(format!(
23533 "expected MATERIALIZED after AS NOT, got {:?}",
23534 self.peek()
23535 )));
23536 }
23537 } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
23538 if s.eq_ignore_ascii_case("materialized"))
23539 {
23540 self.advance();
23541 }
23542 if !matches!(self.peek(), Token::LParen) {
23543 return Err(self.err(format!(
23544 "expected '(' after AS in WITH clause, got {:?}",
23545 self.peek()
23546 )));
23547 }
23548 self.advance();
23549 // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
23550 // RETURNING) as the CTE body in addition to SELECT.
23551 // PG writable CTE semantics. UPDATE / DELETE come in as
23552 // bare Idents (lexer keeps SELECT / INSERT as reserved
23553 // tokens but treats the rest of DML as case-insensitive
23554 // idents).
23555 let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23556 let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23557 let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23558 let body = match self.peek() {
23559 Token::Select => {
23560 let inner = self.parse_select_stmt()?;
23561 let Statement::Select(s) = inner else {
23562 unreachable!("parse_select_stmt returns Select");
23563 };
23564 crate::ast::CteBody::Select(s)
23565 }
23566 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23567 // `SELECT * FROM t` this way and accepts it wherever a
23568 // SELECT goes, so the CTE body dispatch needs its own
23569 // arm: this match is keyed on the FIRST token, and
23570 // `Token::Table` fell through to a tail that then
23571 // rejected what it got. `parse_table_shorthand` has
23572 // returned a desugared SelectStatement since the
23573 // shorthand landed — only the routing was missing.
23574 // Round 868 found this by putting the shorthand in a
23575 // subquery; every earlier check used a top-level form.
23576 // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
23577 // `SELECT * FROM t` this way and accepts it wherever a
23578 // SELECT goes, so the CTE body dispatch needs its own
23579 // arm: this match is keyed on the FIRST token, and
23580 // `Token::Table` fell through to a tail that rejected
23581 // what it got. `parse_table_shorthand` has returned a
23582 // desugared SelectStatement since the shorthand landed —
23583 // only the routing was missing, here and in the derived
23584 // table's second-token gate. Round 868 found both by
23585 // putting the shorthand in a subquery; every earlier
23586 // check had used a top-level form.
23587 Token::Table
23588 if matches!(
23589 self.tokens.get(self.pos + 1),
23590 Some(Token::Ident(_) | Token::QuotedIdent(_))
23591 ) =>
23592 {
23593 let mut head = self.parse_table_shorthand()?;
23594 self.parse_setop_chain_into(&mut head)?;
23595 self.parse_select_tail_into(&mut head)?;
23596 crate::ast::CteBody::Select(head)
23597 }
23598 // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
23599 // WITH t(a) AS (VALUES (1), (2)) … lowers through
23600 // the shared rows helper onto a Select body.
23601 Token::Values => {
23602 self.advance(); // VALUES
23603 let mut head = self.parse_values_rows_body()?;
23604 // A VALUES seed can head a set-operation chain —
23605 // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
23606 // SELECT n+1 FROM t …). Attach any trailing
23607 // UNION / INTERSECT / EXCEPT peers so the
23608 // recursive-CTE body parses like the SELECT seed.
23609 self.parse_setop_chain_into(&mut head)?;
23610 crate::ast::CteBody::Select(head)
23611 }
23612 Token::Insert => {
23613 let inner = self.parse_one_statement()?;
23614 let Statement::Insert(s) = inner else {
23615 unreachable!("Token::Insert routes to Insert");
23616 };
23617 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23618 }
23619 _ if is_update_kw => {
23620 let inner = self.parse_one_statement()?;
23621 let Statement::Update(s) = inner else {
23622 return Err(
23623 self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
23624 );
23625 };
23626 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23627 }
23628 _ if is_delete_kw => {
23629 let inner = self.parse_one_statement()?;
23630 let Statement::Delete(s) = inner else {
23631 return Err(
23632 self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
23633 );
23634 };
23635 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23636 }
23637 // v7.39 (round 149) — PG 17 allows MERGE as a
23638 // data-modifying CTE body.
23639 _ if is_merge_kw => {
23640 let inner = self.parse_one_statement()?;
23641 let Statement::Merge(s) = inner else {
23642 return Err(
23643 self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
23644 );
23645 };
23646 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23647 }
23648 // v7.39 (round 151) — a CTE body may itself be
23649 // WITH-headed (PG grammar: PreparableStmt carries its
23650 // own with_clause). The nested statement keeps its own
23651 // ctes; the modifying-CTE-at-top-level rule is enforced
23652 // at execution.
23653 Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
23654 self.advance(); // WITH
23655 match self.parse_with_cte_then_select()? {
23656 Statement::Select(s) => crate::ast::CteBody::Select(s),
23657 Statement::Insert(s) => {
23658 crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
23659 }
23660 Statement::Update(s) => {
23661 crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
23662 }
23663 Statement::Delete(s) => {
23664 crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
23665 }
23666 Statement::Merge(s) => {
23667 crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
23668 }
23669
23670 other => {
23671 return Err(self.err(format!(
23672 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23673 )));
23674 }
23675 }
23676 }
23677 other => {
23678 return Err(self.err(format!(
23679 "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
23680 )));
23681 }
23682 };
23683 if !matches!(self.peek(), Token::RParen) {
23684 return Err(self.err(format!(
23685 "expected ')' after CTE body, got {:?}",
23686 self.peek()
23687 )));
23688 }
23689 self.advance();
23690 // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
23691 // CTE, desugared into extra body columns by the engine.
23692 let search = self.parse_cte_search_clause()?;
23693 let cycle = self.parse_cte_cycle_clause()?;
23694 let mut cte = crate::ast::Cte {
23695 name,
23696 body,
23697 recursive,
23698 column_overrides,
23699 search,
23700 cycle,
23701 };
23702 self.validate_recursive_cte(&cte)?;
23703 self.desugar_cte_search_cycle(&mut cte)?;
23704 ctes.push(cte);
23705 if matches!(self.peek(), Token::Comma) {
23706 self.advance();
23707 continue;
23708 }
23709 break;
23710 }
23711 // v7.37.43-T4.4 — the outer body may be SELECT (classical),
23712 // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
23713 // the parsed CTEs to whichever statement the body produces.
23714 let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
23715 let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
23716 let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
23717 match self.peek() {
23718 Token::Select => {
23719 let body_stmt = self.parse_select_stmt()?;
23720 let Statement::Select(mut body) = body_stmt else {
23721 unreachable!()
23722 };
23723 body.ctes = ctes;
23724 Ok(Statement::Select(body))
23725 }
23726 Token::Insert => {
23727 let body_stmt = self.parse_one_statement()?;
23728 let Statement::Insert(mut body) = body_stmt else {
23729 unreachable!()
23730 };
23731 body.ctes = ctes;
23732 Ok(Statement::Insert(body))
23733 }
23734 _ if outer_is_update => {
23735 let body_stmt = self.parse_one_statement()?;
23736 let Statement::Update(mut body) = body_stmt else {
23737 return Err(self.err(format!("expected UPDATE after WITH clause")));
23738 };
23739 body.ctes = ctes;
23740 Ok(Statement::Update(body))
23741 }
23742 _ if outer_is_delete => {
23743 let body_stmt = self.parse_one_statement()?;
23744 let Statement::Delete(mut body) = body_stmt else {
23745 return Err(self.err(format!("expected DELETE after WITH clause")));
23746 };
23747 body.ctes = ctes;
23748 Ok(Statement::Delete(body))
23749 }
23750 // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
23751 // WITH RECURSIVE is rejected with PG's exact message
23752 // (parse analysis, transformWithClause).
23753 _ if outer_is_merge => {
23754 if recursive {
23755 return Err(self.err(String::from(
23756 "WITH RECURSIVE is not supported for MERGE statement",
23757 )));
23758 }
23759 let body_stmt = self.parse_one_statement()?;
23760 let Statement::Merge(mut body) = body_stmt else {
23761 return Err(self.err(format!("expected MERGE after WITH clause")));
23762 };
23763 body.ctes = ctes;
23764 Ok(Statement::Merge(body))
23765 }
23766 other => Err(self.err(format!(
23767 "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
23768 ))),
23769 }
23770 }
23771
23772 /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
23773 /// already consumed the leading `EXISTS` ident via
23774 /// `self.advance()`.
23775 /// v7.13.0 — parse the rest of a `CASE … END` expression after
23776 /// the leading `CASE` ident has been consumed (mailrs round-5
23777 /// G9). Supports both the searched form
23778 /// (`CASE WHEN cond THEN val …`) and the simple form
23779 /// (`CASE operand WHEN val THEN val …`).
23780 fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
23781 // Disambiguate searched vs simple form: if the next token
23782 // is `WHEN`, we're in the searched form. Otherwise the
23783 // intervening expression is the operand.
23784 let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
23785 None
23786 } else {
23787 Some(Box::new(self.parse_expr(0)?))
23788 };
23789 let mut branches: Vec<(Expr, Expr)> = Vec::new();
23790 loop {
23791 match self.peek() {
23792 Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
23793 self.advance();
23794 let cond = self.parse_expr(0)?;
23795 match self.peek() {
23796 Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
23797 self.advance();
23798 }
23799 other => {
23800 return Err(self.err(alloc::format!(
23801 "expected THEN after CASE WHEN <expr>, got {other:?}"
23802 )));
23803 }
23804 }
23805 let value = self.parse_expr(0)?;
23806 branches.push((cond, value));
23807 }
23808 _ => break,
23809 }
23810 }
23811 if branches.is_empty() {
23812 return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
23813 }
23814 let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
23815 {
23816 self.advance();
23817 Some(Box::new(self.parse_expr(0)?))
23818 } else {
23819 None
23820 };
23821 match self.peek() {
23822 Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
23823 self.advance();
23824 }
23825 other => {
23826 return Err(self.err(alloc::format!(
23827 "expected END to close CASE expression, got {other:?}"
23828 )));
23829 }
23830 }
23831 Ok(Expr::Case {
23832 operand,
23833 branches,
23834 else_branch,
23835 })
23836 }
23837
23838 /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
23839 /// query-source position (EXISTS / IN / INSERT source / CTE body /
23840 /// view body). Caller consumed the WITH keyword. Only a SELECT
23841 /// outer is grammatical here; the data-modifying-CTE-at-top-level
23842 /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
23843 /// maps correctly.
23844 fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23845 let inner = self.parse_with_cte_then_select()?;
23846 match inner {
23847 Statement::Select(s) => Ok(s),
23848 other => Err(self.err(format!(
23849 "expected SELECT after WITH in a subquery, got {other:?}"
23850 ))),
23851 }
23852 }
23853
23854 /// True when the next token is the (unquoted) WITH keyword. WITH is
23855 /// reserved in PG, so a bare `with` can never be a column reference
23856 /// in these positions; a quoted `"with"` stays an identifier.
23857 fn peek_is_with_kw(&self) -> bool {
23858 matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
23859 }
23860
23861 /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
23862 /// `#[inline(never)]` keeps the large SelectStatement temporaries
23863 /// off parse_expr's recursive frame (the nesting-budget stack
23864 /// cliff — see the round-153 gate regression).
23865 #[inline(never)]
23866 fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
23867 if self.peek_is_with_kw() {
23868 self.advance();
23869 self.parse_nested_with_select()
23870 } else {
23871 match self.parse_select_stmt()? {
23872 Statement::Select(s) => Ok(s),
23873 other => Err(self.err(alloc::format!(
23874 "expected SELECT inside ANY/ALL, got {other:?}"
23875 ))),
23876 }
23877 }
23878 }
23879
23880 fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
23881 if !matches!(self.peek(), Token::LParen) {
23882 return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
23883 }
23884 self.advance();
23885 // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
23886 let s = if self.peek_is_with_kw() {
23887 self.advance();
23888 self.parse_nested_with_select()?
23889 } else {
23890 let inner = self.parse_select_stmt()?;
23891 let Statement::Select(s) = inner else {
23892 unreachable!("parse_select_stmt returns Select")
23893 };
23894 s
23895 };
23896 if !matches!(self.peek(), Token::RParen) {
23897 return Err(self.err(format!(
23898 "expected ')' after EXISTS-subquery, got {:?}",
23899 self.peek()
23900 )));
23901 }
23902 self.advance();
23903 Ok(Expr::Exists {
23904 subquery: Box::new(s),
23905 negated,
23906 })
23907 }
23908
23909 fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23910 self.advance(); // IN
23911 if !matches!(self.peek(), Token::LParen) {
23912 return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
23913 }
23914 self.advance();
23915 // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
23916 // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
23917 if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
23918 let s = if self.peek_is_with_kw() {
23919 self.advance();
23920 self.parse_nested_with_select()?
23921 } else {
23922 let inner = self.parse_select_stmt()?;
23923 let Statement::Select(s) = inner else {
23924 unreachable!("parse_select_stmt always returns Statement::Select")
23925 };
23926 s
23927 };
23928 if !matches!(self.peek(), Token::RParen) {
23929 return Err(self.err(format!(
23930 "expected ')' after IN-subquery, got {:?}",
23931 self.peek()
23932 )));
23933 }
23934 self.advance();
23935 return Ok(Expr::InSubquery {
23936 expr: Box::new(expr),
23937 subquery: Box::new(s),
23938 negated,
23939 });
23940 }
23941 let mut elements = Vec::new();
23942 if !matches!(self.peek(), Token::RParen) {
23943 loop {
23944 elements.push(self.parse_expr(0)?);
23945 match self.peek() {
23946 Token::Comma => {
23947 self.advance();
23948 }
23949 Token::RParen => break,
23950 other => {
23951 return Err(
23952 self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
23953 );
23954 }
23955 }
23956 }
23957 }
23958 self.advance(); // ')'
23959 // v7.30.2 (mailrs round-25) — flat InList node instead of a
23960 // left-deep OR-Eq chain: chain depth scaled with the element
23961 // count and overflowed the stack (eval + drop are recursive).
23962 if elements.is_empty() {
23963 return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
23964 }
23965 Ok(Expr::InList {
23966 expr: Box::new(expr),
23967 list: elements,
23968 negated,
23969 })
23970 }
23971
23972 /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
23973 /// already consumed by the caller. Elements must be numeric literals
23974 /// (with optional unary `-`); any compound expression is rejected at
23975 /// parse time so the runtime never needs to evaluate inside a vector.
23976 /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
23977 /// has already consumed the `EXTRACT` token before calling us —
23978 /// we pick up at the opening `(`.
23979 /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
23980 /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
23981 /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
23982 /// per-column OR-fold of
23983 /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23984 /// term)` so the existing FTS evaluator handles semantics.
23985 ///
23986 /// The mode modifier is accepted-and-ignored at v7.17 — all
23987 /// modes map to the same `plainto_tsquery` rewrite. Boolean-
23988 /// mode operators (`+foo -bar`) would need their own parser
23989 /// (Phase 2.2c); customers who hit them today already get a
23990 /// correct lexeme-match against the bare term, only without
23991 /// the +/- precedence the customer asked for.
23992 fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
23993 // Already at `MATCH`-consumed position; the dispatcher
23994 // confirmed the next token is `(`.
23995 if !matches!(self.peek(), Token::LParen) {
23996 return Err(self.err(alloc::format!(
23997 "expected '(' after MATCH, got {:?}",
23998 self.peek()
23999 )));
24000 }
24001 self.advance();
24002 let mut cols: Vec<Expr> = Vec::new();
24003 loop {
24004 cols.push(self.parse_expr(0)?);
24005 match self.peek() {
24006 Token::Comma => {
24007 self.advance();
24008 }
24009 Token::RParen => break,
24010 other => {
24011 return Err(self.err(alloc::format!(
24012 "expected ',' or ')' in MATCH column list, got {other:?}"
24013 )));
24014 }
24015 }
24016 }
24017 self.advance(); // ')'
24018 // Expect AGAINST.
24019 match self.peek() {
24020 Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24021 self.advance();
24022 }
24023 other => {
24024 return Err(self.err(alloc::format!(
24025 "expected AGAINST after MATCH column list, got {other:?}"
24026 )));
24027 }
24028 }
24029 if !matches!(self.peek(), Token::LParen) {
24030 return Err(self.err(alloc::format!(
24031 "expected '(' after AGAINST, got {:?}",
24032 self.peek()
24033 )));
24034 }
24035 self.advance();
24036 // Read AGAINST's argument as a single primary token —
24037 // string literal, placeholder, or column-ref ident. We
24038 // can't call `parse_expr` / `parse_unary` here because
24039 // the postfix chain inside `parse_atom` would greedily
24040 // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24041 // and fail at "expected '(' after IN". Customers always
24042 // write a literal or bound parameter in AGAINST, so this
24043 // restriction is non-blocking; the error path explains
24044 // the limit if a more complex expression shows up.
24045 let term = match self.advance() {
24046 Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24047 Token::Placeholder(n) => Expr::Placeholder(n),
24048 Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24049 qualifier: None,
24050 name: s,
24051 }),
24052 other => {
24053 return Err(self.err(alloc::format!(
24054 "MATCH ... AGAINST(<term>) expects a string literal, \
24055 bound parameter, or column ref, got {other:?}"
24056 )));
24057 }
24058 };
24059 // Optional mode tail — accept-and-ignore at v7.17:
24060 // IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24061 // IN BOOLEAN MODE
24062 // WITH QUERY EXPANSION
24063 loop {
24064 match self.peek() {
24065 // IN lexes as a reserved Token::In, not an ident,
24066 // so it gets its own arm.
24067 Token::In => {
24068 self.advance();
24069 }
24070 Token::Ident(s) | Token::QuotedIdent(s)
24071 if s.eq_ignore_ascii_case("natural")
24072 || s.eq_ignore_ascii_case("language")
24073 || s.eq_ignore_ascii_case("boolean")
24074 || s.eq_ignore_ascii_case("mode")
24075 || s.eq_ignore_ascii_case("with")
24076 || s.eq_ignore_ascii_case("query")
24077 || s.eq_ignore_ascii_case("expansion") =>
24078 {
24079 self.advance();
24080 }
24081 _ => break,
24082 }
24083 }
24084 if !matches!(self.peek(), Token::RParen) {
24085 return Err(self.err(alloc::format!(
24086 "expected ')' to close AGAINST, got {:?}",
24087 self.peek()
24088 )));
24089 }
24090 self.advance();
24091 // Build per-column `to_tsvector('simple', col) @@
24092 // plainto_tsquery('simple', term)` and OR-fold.
24093 let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24094 let plainto = Expr::FunctionCall {
24095 name: String::from("plainto_tsquery"),
24096 args: alloc::vec![simple_lit(), term.clone()],
24097 };
24098 let mut folded: Option<Expr> = None;
24099 for col in cols {
24100 let to_tsv = Expr::FunctionCall {
24101 name: String::from("to_tsvector"),
24102 args: alloc::vec![simple_lit(), col],
24103 };
24104 let leaf = Expr::Binary {
24105 lhs: Box::new(to_tsv),
24106 op: crate::ast::BinOp::TsMatch,
24107 rhs: Box::new(plainto.clone()),
24108 };
24109 folded = Some(match folded {
24110 None => leaf,
24111 Some(prev) => Expr::Binary {
24112 lhs: Box::new(prev),
24113 op: crate::ast::BinOp::Or,
24114 rhs: Box::new(leaf),
24115 },
24116 });
24117 }
24118 match folded {
24119 Some(e) => Ok(e),
24120 None => Err(self.err(String::from(
24121 "MATCH(...) AGAINST(...) requires at least one column",
24122 ))),
24123 }
24124 }
24125
24126 fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24127 if !matches!(self.peek(), Token::LParen) {
24128 return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24129 }
24130 self.advance();
24131 let field_name = self.expect_ident_like()?;
24132 let field = match field_name.to_ascii_lowercase().as_str() {
24133 // PG accepts the plural spellings (years/months/…/millenniums) as
24134 // aliases for the singular fields — its datetime unit table has both.
24135 // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24136 "year" | "years" => ExtractField::Year,
24137 "month" | "months" => ExtractField::Month,
24138 "day" | "days" => ExtractField::Day,
24139 "hour" | "hours" => ExtractField::Hour,
24140 "minute" | "minutes" => ExtractField::Minute,
24141 "second" | "seconds" => ExtractField::Second,
24142 "microsecond" | "microseconds" => ExtractField::Microsecond,
24143 "epoch" => ExtractField::Epoch,
24144 "dow" => ExtractField::Dow,
24145 "isodow" => ExtractField::Isodow,
24146 "doy" => ExtractField::Doy,
24147 "week" | "weeks" => ExtractField::Week,
24148 "isoyear" => ExtractField::Isoyear,
24149 "quarter" => ExtractField::Quarter,
24150 "decade" | "decades" => ExtractField::Decade,
24151 "century" | "centuries" => ExtractField::Century,
24152 "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24153 "julian" => ExtractField::Julian,
24154 "millisecond" | "milliseconds" => ExtractField::Millisecond,
24155 "timezone" => ExtractField::Timezone,
24156 "timezone_hour" => ExtractField::TimezoneHour,
24157 "timezone_minute" => ExtractField::TimezoneMinute,
24158 // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24159 // reports an unknown one with the source type (22023); carry the
24160 // raw name so eval can word it.
24161 other => ExtractField::Other(alloc::string::String::from(other)),
24162 };
24163 if !matches!(self.peek(), Token::From) {
24164 return Err(self.err(format!(
24165 "expected FROM after EXTRACT field, got {:?}",
24166 self.peek()
24167 )));
24168 }
24169 self.advance();
24170 let source = self.parse_expr(0)?;
24171 if !matches!(self.peek(), Token::RParen) {
24172 return Err(self.err(format!(
24173 "expected ')' to close EXTRACT, got {:?}",
24174 self.peek()
24175 )));
24176 }
24177 self.advance();
24178 Ok(Expr::Extract {
24179 field,
24180 source: Box::new(source),
24181 })
24182 }
24183
24184 /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24185 /// is already consumed; we expect a single string literal next and
24186 /// resolve it into `Literal::Interval` at parse time so the engine
24187 /// never has to re-tokenise inside the string.
24188 /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24189 /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24190 /// is the SQL-standard form and is left to the path below.
24191 fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24192 // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24193 let (offset, sign) = match self.peek() {
24194 Token::Minus => (1, "-"),
24195 _ => (0, ""),
24196 };
24197 let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24198 return None;
24199 };
24200 self.tokens
24201 .get(self.pos + offset + 1)
24202 .filter(|t| mysql_interval_unit(t).is_some())?;
24203 Some((alloc::format!("{sign}{n}"), offset + 1))
24204 }
24205
24206 /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24207 /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24208 /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24209 ///
24210 /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24211 /// this by parsing the group and then restoring `self.pos` — which could
24212 /// never have worked, because `advance()` DESTROYS the token it returns
24213 /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24214 /// inert only because both branches errored back then.
24215 fn interval_paren_is_quantity(&self) -> bool {
24216 let mut depth = 0usize;
24217 let mut saw_top_level_comma = false;
24218 let mut i = self.pos;
24219 while let Some(tok) = self.tokens.get(i) {
24220 match tok {
24221 Token::LParen => depth += 1,
24222 Token::RParen => {
24223 depth = depth.saturating_sub(1);
24224 if depth == 0 {
24225 return !saw_top_level_comma
24226 && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
24227 .is_some();
24228 }
24229 }
24230 // A comma directly inside the outermost parens means the
24231 // argument list of the INTERVAL() function.
24232 Token::Comma if depth == 1 => saw_top_level_comma = true,
24233 Token::Eof => return false,
24234 _ => {}
24235 }
24236 i += 1;
24237 }
24238 false
24239 }
24240
24241 fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
24242 // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
24243 // (the index of the last Ni ≤ N), distinct from the interval literal.
24244 // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
24245 // is decided by a non-destructive lookahead (round 422) before either
24246 // branch consumes anything. MySQL only.
24247 if self.mysql_dialect
24248 && matches!(self.peek(), Token::LParen)
24249 && !self.interval_paren_is_quantity()
24250 {
24251 self.advance(); // (
24252 let mut args = Vec::new();
24253 if !matches!(self.peek(), Token::RParen) {
24254 loop {
24255 args.push(self.parse_expr(0)?);
24256 if matches!(self.peek(), Token::Comma) {
24257 self.advance();
24258 continue;
24259 }
24260 break;
24261 }
24262 }
24263 if !matches!(self.peek(), Token::RParen) {
24264 return Err(self.err(alloc::format!(
24265 "expected ')' after INTERVAL() arguments, got {:?}",
24266 self.peek()
24267 )));
24268 }
24269 self.advance(); // )
24270 return Ok(Expr::FunctionCall {
24271 name: alloc::string::String::from("interval"),
24272 args,
24273 });
24274 }
24275 // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
24276 // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
24277 // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
24278 // writes every date arithmetic there is, and it did not parse at
24279 // all. PG rejects the unquoted form outright (`syntax error at or
24280 // near "1"`, measured), so it is taken only in the MySQL dialect —
24281 // PG's own `INTERVAL '1' DAY` is untouched below.
24282 if self.mysql_dialect
24283 && let Some((text, consume)) = self.peek_unquoted_interval_count()
24284 {
24285 for _ in 0..consume {
24286 self.advance(); // the optional `-` and the number
24287 }
24288 let Some(unit) = mysql_interval_unit(self.peek()) else {
24289 return Err(self.err(alloc::format!(
24290 "expected an interval unit after INTERVAL {text}, got {:?}",
24291 self.peek()
24292 )));
24293 };
24294 self.advance(); // the unit
24295 let (months, days, micros) = scale_mysql_interval(&text, unit)
24296 .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
24297 return Ok(Expr::Literal(Literal::Interval {
24298 months,
24299 days,
24300 micros,
24301 // The canonical rendering, so Display round-trips into a
24302 // form both dialects read back.
24303 text: alloc::format!("{text} {unit}"),
24304 }));
24305 }
24306 // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
24307 // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
24308 // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
24309 // Those cannot fold into a compile-time `Literal::Interval`, so they
24310 // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
24311 // builtin, which builds the value at run time (and yields NULL for a
24312 // NULL quantity, as MariaDB does). The literal path above still folds
24313 // the constant case — it is cheaper and round-trips through Display.
24314 //
24315 // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
24316 // MySQL's quoted spelling) keep the qualifier path below.
24317 if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
24318 let qty = self.parse_expr(0)?;
24319 let Some(unit) = mysql_interval_unit(self.peek()) else {
24320 return Err(self.err(alloc::format!(
24321 "expected an interval unit after INTERVAL <expr>, got {:?}",
24322 self.peek()
24323 )));
24324 };
24325 self.advance(); // the unit
24326 return Ok(make_interval_call(qty, unit));
24327 }
24328 let tok = self.advance();
24329 let Token::String(text) = tok else {
24330 return Err(self.err(format!(
24331 "expected string literal after INTERVAL, got {tok:?}"
24332 )));
24333 };
24334 // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
24335 // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
24336 // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
24337 // bare number means and the leading/trailing precision.
24338 let field1 = interval_field_of(self.peek());
24339 let qualifier = if let Some(f1) = field1 {
24340 self.advance();
24341 let f2 = if matches!(self.peek(), Token::To) {
24342 self.advance();
24343 let Some(f) = interval_field_of(self.peek()) else {
24344 return Err(self.err(format!(
24345 "expected an interval field after TO, got {:?}",
24346 self.peek()
24347 )));
24348 };
24349 self.advance();
24350 Some(f)
24351 } else {
24352 None
24353 };
24354 Some((f1, f2))
24355 } else {
24356 None
24357 };
24358 let (months, days, micros) = match qualifier {
24359 Some(q) => interpret_qualified_interval(&text, q),
24360 None => parse_interval_text(&text),
24361 }
24362 .ok_or_else(|| ParseError {
24363 message: format!(
24364 "cannot parse INTERVAL {text:?}; \
24365 expected `<n> <unit> [<n> <unit> ...]` with units \
24366 microsecond[s], millisecond[s], second[s], minute[s], \
24367 hour[s], day[s], week[s], month[s], year[s]"
24368 ),
24369 token_pos: self.consumed_pos(),
24370 })?;
24371 Ok(Expr::Literal(Literal::Interval {
24372 months,
24373 days,
24374 micros,
24375 text,
24376 }))
24377 }
24378
24379 /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
24380 /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
24381 /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
24382 /// than a pgvector literal.
24383 fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
24384 self.advance(); // consume `[`
24385 let mut items: Vec<Expr> = Vec::new();
24386 if !matches!(self.peek(), Token::RBracket) {
24387 loop {
24388 if matches!(self.peek(), Token::LBracket) {
24389 items.push(self.parse_array_bracket_body()?);
24390 } else {
24391 items.push(self.parse_expr(0)?);
24392 }
24393 match self.peek() {
24394 Token::Comma => {
24395 self.advance();
24396 }
24397 Token::RBracket => break,
24398 other => {
24399 return Err(self.err(alloc::format!(
24400 "expected ',' or ']' in array literal, got {other:?}"
24401 )));
24402 }
24403 }
24404 }
24405 }
24406 self.advance(); // consume `]`
24407 Ok(Expr::Array(items))
24408 }
24409
24410 fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
24411 let mut elems = Vec::new();
24412 if matches!(self.peek(), Token::RBracket) {
24413 self.advance();
24414 return Ok(Expr::Literal(Literal::Vector(elems)));
24415 }
24416 loop {
24417 let e = self.parse_expr(0)?;
24418 let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
24419 message: format!("vector element must be a numeric literal, got {e:?}"),
24420 token_pos: self.pos,
24421 })?;
24422 elems.push(x);
24423 match self.peek() {
24424 Token::Comma => {
24425 self.advance();
24426 }
24427 Token::RBracket => {
24428 self.advance();
24429 break;
24430 }
24431 other => {
24432 return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
24433 }
24434 }
24435 }
24436 Ok(Expr::Literal(Literal::Vector(elems)))
24437 }
24438
24439 /// Atom that started with an identifier: could be `t.col`, `col`, or
24440 /// `func(arg, ...)`. Detect each shape by looking at the next token.
24441 /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
24442 /// [, ...])`. Caller has already consumed `OVER`. Either clause
24443 /// is optional; an empty `()` is also legal (PG semantics).
24444 /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
24445 /// modifier between `name(args)` and `OVER (...)`. Default is
24446 /// `Respect`. Unrecognised idents leave the stream unchanged.
24447 fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
24448 let Token::Ident(s) = self.peek().clone() else {
24449 return NullTreatment::Respect;
24450 };
24451 let is_ignore = s.eq_ignore_ascii_case("ignore");
24452 let is_respect = s.eq_ignore_ascii_case("respect");
24453 if !is_ignore && !is_respect {
24454 return NullTreatment::Respect;
24455 }
24456 // Lookahead for NULLS — only consume both tokens together.
24457 // pos+1 must hold a "nulls" ident.
24458 if self.pos + 1 < self.tokens.len()
24459 && let Token::Ident(s2) = &self.tokens[self.pos + 1]
24460 && s2.eq_ignore_ascii_case("nulls")
24461 {
24462 self.advance();
24463 self.advance();
24464 return if is_ignore {
24465 NullTreatment::Ignore
24466 } else {
24467 NullTreatment::Respect
24468 };
24469 }
24470 NullTreatment::Respect
24471 }
24472
24473 /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
24474 /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
24475 /// (same shape as the `OVER` tail). Consumes the whole clause and
24476 /// returns the predicate; returns `None` when no `FILTER` follows.
24477 fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
24478 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24479 return Ok(None);
24480 };
24481 if !s.eq_ignore_ascii_case("filter") {
24482 return Ok(None);
24483 }
24484 self.advance(); // FILTER
24485 if !matches!(self.peek(), Token::LParen) {
24486 return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
24487 }
24488 self.advance(); // (
24489 if !matches!(self.peek(), Token::Where) {
24490 return Err(self.err(format!(
24491 "expected WHERE inside FILTER (...), got {:?}",
24492 self.peek()
24493 )));
24494 }
24495 self.advance(); // WHERE
24496 let cond = self.parse_expr(0)?;
24497 if !matches!(self.peek(), Token::RParen) {
24498 return Err(self.err(format!(
24499 "expected ')' to close FILTER (WHERE ...), got {:?}",
24500 self.peek()
24501 )));
24502 }
24503 self.advance(); // )
24504 Ok(Some(Box::new(cond)))
24505 }
24506
24507 /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
24508 /// the separator as the aggregate's second argument, which is the
24509 /// shape `string_agg` already takes. Returns whether one was there.
24510 fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
24511 if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
24512 return Ok(false);
24513 }
24514 self.advance();
24515 let Token::String(sep) = self.peek().clone() else {
24516 return Err(self.err(alloc::format!(
24517 "expected a string literal after SEPARATOR, got {:?}",
24518 self.peek()
24519 )));
24520 };
24521 self.advance();
24522 args.push(Expr::Literal(Literal::String(sep)));
24523 Ok(true)
24524 }
24525
24526 /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
24527 /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
24528 /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
24529 /// keys, or an empty vec when no `WITHIN GROUP` follows.
24530 fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
24531 let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
24532 return Ok(Vec::new());
24533 };
24534 if !s.eq_ignore_ascii_case("within") {
24535 return Ok(Vec::new());
24536 }
24537 self.advance(); // WITHIN
24538 if !matches!(self.peek(), Token::Group) {
24539 return Err(self.err(format!(
24540 "expected GROUP after WITHIN, got {:?}",
24541 self.peek()
24542 )));
24543 }
24544 self.advance(); // GROUP
24545 if !matches!(self.peek(), Token::LParen) {
24546 return Err(self.err(format!(
24547 "expected '(' after WITHIN GROUP, got {:?}",
24548 self.peek()
24549 )));
24550 }
24551 self.advance(); // (
24552 if !matches!(self.peek(), Token::Order) {
24553 return Err(self.err(format!(
24554 "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
24555 self.peek()
24556 )));
24557 }
24558 self.advance(); // ORDER
24559 if !self.peek_is_by() {
24560 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24561 }
24562 self.advance(); // BY
24563 let mut keys: Vec<OrderBy> = Vec::new();
24564 loop {
24565 // v7.39 (round 691) — save/restore, the discipline this parser
24566 // already uses around `pending_sample_preds`, so a subquery inside
24567 // a key neither inherits nor leaks the channel.
24568 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
24569 let saved_coll = self.order_key_collation.take();
24570 let parsed = self.parse_expr(0);
24571 self.in_order_by_key = saved_flag;
24572 let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
24573 let expr = parsed?;
24574 let desc = if matches!(self.peek(), Token::Desc) {
24575 self.advance();
24576 true
24577 } else if matches!(self.peek(), Token::Asc) {
24578 self.advance();
24579 false
24580 } else {
24581 false
24582 };
24583 let nulls_first = self.parse_optional_nulls_placement()?;
24584 keys.push(OrderBy {
24585 expr,
24586 desc,
24587 nulls_first,
24588 collation,
24589 });
24590 if matches!(self.peek(), Token::Comma) {
24591 self.advance();
24592 } else {
24593 break;
24594 }
24595 }
24596 if !matches!(self.peek(), Token::RParen) {
24597 return Err(self.err(format!(
24598 "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
24599 self.peek()
24600 )));
24601 }
24602 self.advance(); // )
24603 Ok(keys)
24604 }
24605
24606 /// No frame clause is supported.
24607 #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
24608 fn parse_over_clause(
24609 &mut self,
24610 ) -> Result<
24611 (
24612 Vec<Expr>,
24613 Vec<(Expr, bool, Option<bool>)>,
24614 Option<WindowFrame>,
24615 ),
24616 ParseError,
24617 > {
24618 // `OVER w` — a named-window reference. The WINDOW clause
24619 // parses after the select list, so the name rides out as a
24620 // marker in partition_by; parse_bare_select substitutes the
24621 // definition once the clause is known.
24622 if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
24623 let name = w.clone();
24624 self.advance();
24625 return Ok((
24626 alloc::vec![Expr::Column(crate::ast::ColumnName {
24627 qualifier: Some("__named_window__".to_string()),
24628 name,
24629 })],
24630 Vec::new(),
24631 None,
24632 ));
24633 }
24634 if !matches!(self.peek(), Token::LParen) {
24635 return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
24636 }
24637 self.advance();
24638 let mut partition_by = Vec::new();
24639 let mut order_by = Vec::new();
24640 // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
24641 // window, refined in place. PG's rules (probed against 18.4) differ
24642 // from the bare `OVER w1` form, so the reference rides out under its
24643 // own marker and `substitute_named_windows` applies them. The base
24644 // name is any leading identifier that isn't a window-spec keyword.
24645 let base_window = match self.peek() {
24646 Token::Ident(s) | Token::QuotedIdent(s)
24647 if !s.eq_ignore_ascii_case("partition")
24648 && !s.eq_ignore_ascii_case("rows")
24649 && !s.eq_ignore_ascii_case("range")
24650 && !s.eq_ignore_ascii_case("groups") =>
24651 {
24652 let n = s.clone();
24653 self.advance();
24654 Some(n)
24655 }
24656 _ => None,
24657 };
24658 // PARTITION BY ?
24659 // v7.37.6-B promoted PARTITION to a reserved keyword
24660 // (Token::Partition); pre-7.37.6-B catalogs lexed it as
24661 // `Token::Ident("partition")`. Accept both so older sources
24662 // and the new lexer surface land on the same path.
24663 let is_partition_kw = match self.peek() {
24664 Token::Partition => true,
24665 Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
24666 _ => false,
24667 };
24668 if is_partition_kw {
24669 self.advance();
24670 if !self.peek_is_by() {
24671 return Err(self.err(format!(
24672 "expected BY after PARTITION, got {:?}",
24673 self.peek()
24674 )));
24675 }
24676 self.advance();
24677 loop {
24678 partition_by.push(self.parse_expr(0)?);
24679 if matches!(self.peek(), Token::Comma) {
24680 self.advance();
24681 continue;
24682 }
24683 break;
24684 }
24685 }
24686 // ORDER BY ?
24687 if matches!(self.peek(), Token::Order) {
24688 self.advance();
24689 if !self.peek_is_by() {
24690 return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
24691 }
24692 self.advance();
24693 loop {
24694 let e = self.parse_expr(0)?;
24695 let desc = if matches!(self.peek(), Token::Desc) {
24696 self.advance();
24697 true
24698 } else if matches!(self.peek(), Token::Asc) {
24699 self.advance();
24700 false
24701 } else {
24702 false
24703 };
24704 // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
24705 let nulls_first = self.parse_optional_nulls_placement()?;
24706 order_by.push((e, desc, nulls_first));
24707 if matches!(self.peek(), Token::Comma) {
24708 self.advance();
24709 continue;
24710 }
24711 break;
24712 }
24713 }
24714 // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
24715 // Both keywords come through the lexer as identifiers; match
24716 // case-insensitively.
24717 let mut frame: Option<WindowFrame> = None;
24718 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
24719 let kind = if s.eq_ignore_ascii_case("rows") {
24720 Some(FrameKind::Rows)
24721 } else if s.eq_ignore_ascii_case("range") {
24722 Some(FrameKind::Range)
24723 } else if s.eq_ignore_ascii_case("groups") {
24724 // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
24725 Some(FrameKind::Groups)
24726 } else {
24727 None
24728 };
24729 if let Some(kind) = kind {
24730 self.advance();
24731 frame = Some(self.parse_frame_tail(kind)?);
24732 }
24733 }
24734 if !matches!(self.peek(), Token::RParen) {
24735 return Err(self.err(format!(
24736 "expected ')' to close OVER clause, got {:?}",
24737 self.peek()
24738 )));
24739 }
24740 self.advance();
24741 if let Some(base) = base_window {
24742 // A copy may refine but never override the base's partitioning
24743 // (PG rejects it outright, before looking the name up).
24744 if !partition_by.is_empty() {
24745 return Err(self.err(alloc::format!(
24746 "cannot override PARTITION BY clause of window \"{base}\""
24747 )));
24748 }
24749 partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
24750 qualifier: Some("__named_window_ref__".to_string()),
24751 name: base,
24752 })];
24753 }
24754 Ok((partition_by, order_by, frame))
24755 }
24756
24757 /// v4.20: parse the tail of an explicit frame, given the `ROWS`
24758 /// or `RANGE` keyword was just consumed. Accepts both
24759 /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
24760 /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
24761 /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
24762 fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
24763 let (start, end) = if matches!(self.peek(), Token::Between) {
24764 self.advance();
24765 let start = self.parse_frame_bound()?;
24766 if !matches!(self.peek(), Token::And) {
24767 return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
24768 }
24769 self.advance();
24770 let end = self.parse_frame_bound()?;
24771 (start, Some(end))
24772 } else {
24773 (self.parse_frame_bound()?, None)
24774 };
24775 let exclude = self.parse_frame_exclusion()?;
24776 Ok(WindowFrame {
24777 kind,
24778 start,
24779 end,
24780 exclude,
24781 })
24782 }
24783
24784 /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
24785 /// after a frame spec. NO OTHERS is the default no-op.
24786 fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
24787 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
24788 return Ok(FrameExclusion::NoOthers);
24789 }
24790 self.advance(); // EXCLUDE
24791 match self.peek() {
24792 Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
24793 self.advance();
24794 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
24795 return Err(self.err(format!(
24796 "expected ROW after EXCLUDE CURRENT, got {:?}",
24797 self.peek()
24798 )));
24799 }
24800 self.advance();
24801 Ok(FrameExclusion::CurrentRow)
24802 }
24803 // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
24804 // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
24805 // Without this arm it fell to the catch-all, whose message
24806 // self-contradictingly listed GROUP as expected.
24807 Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
24808 self.advance();
24809 Ok(FrameExclusion::Group)
24810 }
24811 Token::Group => {
24812 self.advance();
24813 Ok(FrameExclusion::Group)
24814 }
24815 Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
24816 self.advance();
24817 Ok(FrameExclusion::Ties)
24818 }
24819 Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
24820 self.advance();
24821 if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
24822 return Err(self.err(format!(
24823 "expected OTHERS after EXCLUDE NO, got {:?}",
24824 self.peek()
24825 )));
24826 }
24827 self.advance();
24828 Ok(FrameExclusion::NoOthers)
24829 }
24830 other => Err(self.err(format!(
24831 "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
24832 ))),
24833 }
24834 }
24835
24836 /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
24837 /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
24838 /// `UNBOUNDED FOLLOWING`.
24839 fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
24840 // Interval-typed offset for a value-based RANGE frame over a
24841 // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
24842 // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
24843 // PRECEDING`.
24844 if let Some((months, days, micros)) = self.try_take_interval_offset()? {
24845 let dir = self.expect_ident_like()?;
24846 return if dir.eq_ignore_ascii_case("preceding") {
24847 Ok(FrameBound::IntervalPreceding {
24848 months,
24849 days,
24850 micros,
24851 })
24852 } else if dir.eq_ignore_ascii_case("following") {
24853 Ok(FrameBound::IntervalFollowing {
24854 months,
24855 days,
24856 micros,
24857 })
24858 } else {
24859 Err(self.err(format!(
24860 "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
24861 )))
24862 };
24863 }
24864 // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
24865 if let Token::Integer(n) = *self.peek() {
24866 self.advance();
24867 let n: u64 = u64::try_from(n).map_err(|_| {
24868 self.err(format!(
24869 "invalid frame offset {n} — expected non-negative integer"
24870 ))
24871 })?;
24872 let dir = self.expect_ident_like()?;
24873 return if dir.eq_ignore_ascii_case("preceding") {
24874 Ok(FrameBound::OffsetPreceding(n))
24875 } else if dir.eq_ignore_ascii_case("following") {
24876 Ok(FrameBound::OffsetFollowing(n))
24877 } else {
24878 Err(self.err(format!(
24879 "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
24880 )))
24881 };
24882 }
24883 let first = self.expect_ident_like()?;
24884 if first.eq_ignore_ascii_case("unbounded") {
24885 let dir = self.expect_ident_like()?;
24886 return if dir.eq_ignore_ascii_case("preceding") {
24887 Ok(FrameBound::UnboundedPreceding)
24888 } else if dir.eq_ignore_ascii_case("following") {
24889 Ok(FrameBound::UnboundedFollowing)
24890 } else {
24891 Err(self.err(format!(
24892 "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
24893 )))
24894 };
24895 }
24896 if first.eq_ignore_ascii_case("current") {
24897 let row = self.expect_ident_like()?;
24898 if !row.eq_ignore_ascii_case("row") {
24899 return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
24900 }
24901 return Ok(FrameBound::CurrentRow);
24902 }
24903 Err(self.err(format!(
24904 "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
24905 )))
24906 }
24907
24908 /// Detect and consume a leading interval offset in a frame bound —
24909 /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
24910 /// `(months, days, micros)`. Leaves the cursor on the trailing
24911 /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
24912 /// when the next tokens are not an interval offset.
24913 fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
24914 // Shape A — `INTERVAL '1 day'`.
24915 if matches!(self.peek(), Token::Interval) {
24916 self.advance(); // INTERVAL
24917 let atom = self.parse_interval_atom()?;
24918 if let Expr::Literal(Literal::Interval {
24919 months,
24920 days,
24921 micros,
24922 ..
24923 }) = atom
24924 {
24925 return Ok(Some((months, days, micros)));
24926 }
24927 return Err(self.err("expected an interval literal in frame offset".to_string()));
24928 }
24929 // Shape B — `'1 day'::interval`. Look ahead for the exact
24930 // string / `::` / interval-target triple before committing.
24931 if let Token::String(text) = self.peek() {
24932 let target_is_interval = match self.tokens.get(self.pos + 2) {
24933 Some(Token::Interval) => true,
24934 Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
24935 _ => false,
24936 };
24937 let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
24938 && target_is_interval;
24939 if is_cast {
24940 let text = text.clone();
24941 self.advance(); // string
24942 self.advance(); // ::
24943 self.advance(); // interval
24944 let parts = parse_interval_text(&text).ok_or_else(|| {
24945 self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
24946 })?;
24947 return Ok(Some(parts));
24948 }
24949 }
24950 Ok(None)
24951 }
24952
24953 fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
24954 if matches!(self.peek(), Token::Dot) {
24955 self.advance();
24956 let name = self.expect_ident_like()?;
24957 // v7.14.0 — schema-qualified function call
24958 // `<schema>.<fn>(args)`. PG dumps emit
24959 // `pg_catalog.set_config(...)` in the preamble. SPG
24960 // is single-namespace: drop the schema prefix and
24961 // route the dispatch on the bare function name.
24962 if matches!(self.peek(), Token::LParen) {
24963 return self.finish_ident_atom(name);
24964 }
24965 return Ok(Expr::Column(ColumnName {
24966 qualifier: Some(first),
24967 name,
24968 }));
24969 }
24970 if matches!(self.peek(), Token::LParen) {
24971 self.advance();
24972 // `COUNT(*)` — special-cased here because `*` isn't a normal
24973 // expression token. Lower-case match on `first` since the lexer
24974 // folds identifiers.
24975 if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
24976 self.advance();
24977 if !matches!(self.peek(), Token::RParen) {
24978 return Err(self.err(format!(
24979 "expected ')' after COUNT(*), got {:?}",
24980 self.peek()
24981 )));
24982 }
24983 self.advance();
24984 // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
24985 let filter = self.parse_filter_clause()?;
24986 // v4.12: COUNT(*) OVER (...) — same window tail.
24987 let null_treatment = self.parse_null_treatment_modifier();
24988 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24989 && s.eq_ignore_ascii_case("over")
24990 {
24991 self.advance();
24992 let (partition_by, order_by, frame) = self.parse_over_clause()?;
24993 return Ok(Expr::WindowFunction {
24994 name: "count_star".into(),
24995 args: Vec::new(),
24996 partition_by,
24997 order_by,
24998 frame,
24999 null_treatment,
25000 filter,
25001 });
25002 }
25003 if let Some(filter) = filter {
25004 return Ok(Expr::AggregateOrdered {
25005 call: Box::new(Expr::FunctionCall {
25006 name: "count_star".into(),
25007 args: Vec::new(),
25008 }),
25009 order_by: Vec::new(),
25010 distinct: false,
25011 filter: Some(filter),
25012 });
25013 }
25014 return Ok(Expr::FunctionCall {
25015 name: "count_star".into(),
25016 args: Vec::new(),
25017 });
25018 }
25019 // Function call. PG-style: zero-or-more comma-separated args.
25020 let mut args = Vec::new();
25021 // v7.38 (read01, T14) — named-argument notation `argname => value`.
25022 // Names are collected in lock-step with `args` and resolved to
25023 // positional order after the loop (the AST stays positional).
25024 let mut arg_names: Vec<Option<String>> = Vec::new();
25025 let mut agg_order_by: Vec<OrderBy> = Vec::new();
25026 // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25027 // seen, so the value arguments before it can be folded.
25028 let mut saw_separator = false;
25029 // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25030 // v7.32 (round-29) — accept the dual `ALL` quantifier too
25031 // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25032 let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25033 self.advance();
25034 true
25035 } else if matches!(self.peek(), Token::All) {
25036 self.advance();
25037 false
25038 } else {
25039 false
25040 };
25041 // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25042 // TIMESTAMPDIFF take a bare unit keyword as the first
25043 // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25044 // bare type keyword (DATE / TIME / DATETIME); lower them
25045 // onto string literals so the evaluator sees plain text.
25046 if ((first.eq_ignore_ascii_case("timestampadd")
25047 || first.eq_ignore_ascii_case("timestampdiff"))
25048 && matches!(self.peek(), Token::Ident(u) if matches!(
25049 u.to_ascii_lowercase().as_str(),
25050 "microsecond" | "second" | "minute" | "hour" | "day"
25051 | "week" | "month" | "quarter" | "year"
25052 )))
25053 || (first.eq_ignore_ascii_case("get_format")
25054 && matches!(self.peek(), Token::Ident(u) if matches!(
25055 u.to_ascii_lowercase().as_str(),
25056 "date" | "time" | "datetime" | "timestamp"
25057 )))
25058 {
25059 if let Token::Ident(u) = self.peek() {
25060 args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25061 }
25062 self.advance();
25063 if matches!(self.peek(), Token::Comma) {
25064 self.advance();
25065 }
25066 }
25067 // `ROW(a, b, …)` keyword constructor. Followed by a
25068 // comparison operator or [NOT] IN it joins the paren
25069 // row-constructor machinery (fieldwise parse-time
25070 // expansion); bare, it stays a `row` call the evaluator
25071 // renders as PG record text.
25072 if first.eq_ignore_ascii_case("row") {
25073 let mut row_items = Vec::new();
25074 if !matches!(self.peek(), Token::RParen) {
25075 loop {
25076 row_items.push(self.parse_expr(0)?);
25077 match self.peek() {
25078 Token::Comma => {
25079 self.advance();
25080 }
25081 Token::RParen => break,
25082 other => {
25083 return Err(self.err(format!(
25084 "expected ',' or ')' in ROW(...), got {other:?}"
25085 )));
25086 }
25087 }
25088 }
25089 }
25090 self.advance(); // ')'
25091 let comparison_follows = matches!(
25092 self.peek(),
25093 Token::Eq
25094 | Token::NotEq
25095 | Token::Lt
25096 | Token::LtEq
25097 | Token::Gt
25098 | Token::GtEq
25099 | Token::In
25100 ) || (matches!(self.peek(), Token::Not)
25101 && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25102 || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25103 if comparison_follows && !row_items.is_empty() {
25104 return self.parse_row_comparison_tail(row_items);
25105 }
25106 return Ok(Expr::FunctionCall {
25107 name: String::from("row"),
25108 args: row_items,
25109 });
25110 }
25111 // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25112 // the parse-mode keyword introduces the source text. SPG
25113 // carries XML as text, so both modes lower to __xmlparse(expr)
25114 // which validates well-formedness and returns Value::Xml.
25115 if first.eq_ignore_ascii_case("xmlparse")
25116 && matches!(self.peek(), Token::Ident(kw)
25117 if kw.eq_ignore_ascii_case("document")
25118 || kw.eq_ignore_ascii_case("content"))
25119 {
25120 let mode = match self.advance() {
25121 Token::Ident(kw) => kw.to_ascii_lowercase(),
25122 _ => unreachable!("peeked an ident"),
25123 };
25124 let src = self.parse_expr(0)?;
25125 if !matches!(self.peek(), Token::RParen) {
25126 return Err(self.err(format!(
25127 "expected ')' to close XMLPARSE, got {:?}",
25128 self.peek()
25129 )));
25130 }
25131 self.advance();
25132 return Ok(Expr::FunctionCall {
25133 name: String::from("__xmlparse"),
25134 args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25135 });
25136 }
25137 // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25138 // keyword introduces the element name (a bare or quoted
25139 // identifier), then optional content expressions. Lower to a
25140 // plain `xmlelement(name_text, content …)` call.
25141 if first.eq_ignore_ascii_case("xmlelement")
25142 && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25143 {
25144 self.advance(); // consume NAME
25145 let elem_name = match self.peek().clone() {
25146 Token::Ident(n) | Token::QuotedIdent(n) => {
25147 self.advance();
25148 n
25149 }
25150 other => {
25151 return Err(self.err(format!(
25152 "expected element name after XMLELEMENT NAME, got {other:?}"
25153 )));
25154 }
25155 };
25156 let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25157 while matches!(self.peek(), Token::Comma) {
25158 self.advance();
25159 args.push(self.parse_expr(0)?);
25160 }
25161 if !matches!(self.peek(), Token::RParen) {
25162 return Err(self.err(format!(
25163 "expected ')' to close XMLELEMENT, got {:?}",
25164 self.peek()
25165 )));
25166 }
25167 self.advance();
25168 return Ok(Expr::FunctionCall {
25169 name: String::from("xmlelement"),
25170 args,
25171 });
25172 }
25173 // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
25174 // becomes a `<name>value</name>` element; a bare column infers its
25175 // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
25176 if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
25177 let mut args: Vec<Expr> = Vec::new();
25178 loop {
25179 let val = self.parse_expr(0)?;
25180 let name = if matches!(self.peek(), Token::As) {
25181 self.advance();
25182 match self.peek().clone() {
25183 Token::Ident(n) | Token::QuotedIdent(n) => {
25184 self.advance();
25185 n
25186 }
25187 other => {
25188 return Err(self.err(format!(
25189 "expected name after AS in XMLFOREST, got {other:?}"
25190 )));
25191 }
25192 }
25193 } else if let Expr::Column(c) = &val {
25194 c.name.clone()
25195 } else {
25196 return Err(
25197 self.err("XMLFOREST element without a column name needs AS".into())
25198 );
25199 };
25200 args.push(Expr::Literal(Literal::String(name)));
25201 args.push(val);
25202 if matches!(self.peek(), Token::Comma) {
25203 self.advance();
25204 } else {
25205 break;
25206 }
25207 }
25208 if !matches!(self.peek(), Token::RParen) {
25209 return Err(self.err(format!(
25210 "expected ')' to close XMLFOREST, got {:?}",
25211 self.peek()
25212 )));
25213 }
25214 self.advance();
25215 return Ok(Expr::FunctionCall {
25216 name: String::from("xmlforest"),
25217 args,
25218 });
25219 }
25220 // SQL-standard `POSITION(sub IN str)` — lowers onto
25221 // strpos(str, sub). IN is the argument separator here,
25222 // so the needle parses with the IN-tail suppressed.
25223 if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
25224 let saved = self.suppress_in_tail;
25225 self.suppress_in_tail = true;
25226 let needle = self.parse_expr(0);
25227 self.suppress_in_tail = saved;
25228 let needle = needle?;
25229 if matches!(self.peek(), Token::In) {
25230 self.advance();
25231 let haystack = self.parse_expr(0)?;
25232 if !matches!(self.peek(), Token::RParen) {
25233 return Err(self.err(format!(
25234 "expected ')' to close POSITION, got {:?}",
25235 self.peek()
25236 )));
25237 }
25238 self.advance();
25239 return Ok(Expr::FunctionCall {
25240 name: String::from("strpos"),
25241 args: alloc::vec![haystack, needle],
25242 });
25243 }
25244 // position(sub, str) comma form (incl. bytea) —
25245 // hand the parsed first arg to the generic list.
25246 args.push(needle);
25247 if matches!(self.peek(), Token::Comma) {
25248 self.advance();
25249 }
25250 }
25251 // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
25252 // FROM str)` — lowers onto btrim / ltrim / rtrim. The
25253 // plain comma forms TRIM(str) / TRIM(str, chars) keep
25254 // riding the generic argument list below.
25255 if first.eq_ignore_ascii_case("trim") {
25256 let mode = match self.peek() {
25257 Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
25258 self.advance();
25259 Some("btrim")
25260 }
25261 Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
25262 self.advance();
25263 Some("ltrim")
25264 }
25265 Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
25266 self.advance();
25267 Some("rtrim")
25268 }
25269 _ => None,
25270 };
25271 if mode.is_some() || matches!(self.peek(), Token::From) {
25272 // TRIM([mode] FROM str) — no strip-chars.
25273 let chars = if matches!(self.peek(), Token::From) {
25274 None
25275 } else {
25276 Some(self.parse_expr(0)?)
25277 };
25278 if !matches!(self.peek(), Token::From) {
25279 return Err(self.err(format!(
25280 "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
25281 self.peek()
25282 )));
25283 }
25284 self.advance();
25285 let target = self.parse_expr(0)?;
25286 if !matches!(self.peek(), Token::RParen) {
25287 return Err(
25288 self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
25289 );
25290 }
25291 self.advance();
25292 let mut trim_args = alloc::vec![target];
25293 if let Some(c) = chars {
25294 trim_args.push(c);
25295 }
25296 return Ok(Expr::FunctionCall {
25297 name: String::from(mode.unwrap_or("btrim")),
25298 args: trim_args,
25299 });
25300 }
25301 }
25302 if !matches!(self.peek(), Token::RParen) {
25303 loop {
25304 // v7.38 (read01, T14) — `argname => value` names this arg.
25305 // v7.39 (read01 round 77) — `argname := value` is the same
25306 // thing, and it is the spelling PG's own docs lead with. It
25307 // was simply never lexed here, so every `f(x := 1)` died in
25308 // the parser regardless of what `f` was.
25309 let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
25310 (
25311 Token::Ident(n) | Token::QuotedIdent(n),
25312 Some(Token::FatArrow | Token::ColonEq),
25313 ) => {
25314 let name = n.clone();
25315 self.advance(); // name
25316 self.advance(); // => / :=
25317 Some(name)
25318 }
25319 _ => None,
25320 };
25321 // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
25322 // array's elements into a variadic call's trailing args
25323 // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
25324 // reserved, so it arrives as a bare ident before the arg.
25325 let is_variadic = this_name.is_none()
25326 && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
25327 if is_variadic {
25328 self.advance();
25329 }
25330 let arg = self.parse_expr(0)?;
25331 args.push(match &this_name {
25332 // The callee's parameter names decide the slot, and a
25333 // user function's live in the catalog. Carry the name
25334 // to eval rather than guessing here.
25335 Some(n) => Expr::NamedArg {
25336 name: n.clone(),
25337 expr: Box::new(arg),
25338 },
25339 None if is_variadic => Expr::Variadic(Box::new(arg)),
25340 None => arg,
25341 });
25342 arg_names.push(this_name);
25343 // v7.25 (round-17) — standard `CAST(expr AS type)`.
25344 // The `::` cast already worked; this lowers the
25345 // function form onto the same Expr::Cast node.
25346 if first.eq_ignore_ascii_case("cast")
25347 && args.len() == 1
25348 && matches!(self.peek(), Token::As)
25349 {
25350 self.advance();
25351 let target = self.parse_cast_target()?;
25352 if !matches!(self.peek(), Token::RParen) {
25353 return Err(self.err(format!(
25354 "expected ')' to close CAST, got {:?}",
25355 self.peek()
25356 )));
25357 }
25358 self.advance();
25359 return Ok(Expr::Cast {
25360 expr: Box::new(args.pop().expect("one arg")),
25361 target,
25362 });
25363 }
25364 // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
25365 // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
25366 // keywords; SPG's lexer makes them plain idents (so they'd be
25367 // read as column refs). Lower the keyword to the string form
25368 // the evaluator already accepts.
25369 if first.eq_ignore_ascii_case("normalize")
25370 && args.len() == 1
25371 && matches!(self.peek(), Token::Comma)
25372 {
25373 let form = match self.tokens.get(self.pos + 1) {
25374 Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
25375 let up = f.to_ascii_uppercase();
25376 matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
25377 }
25378 _ => None,
25379 };
25380 if let Some(up) = form {
25381 self.advance(); // comma
25382 self.advance(); // form keyword
25383 args.push(Expr::Literal(Literal::String(up)));
25384 }
25385 }
25386 // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
25387 // form. Desugars to the comma-list shape evaluator already
25388 // handles. Triggered after the first arg when the function
25389 // name is substring / substr and the next token is FROM
25390 // (a reserved keyword in PG; SPG also reserves it).
25391 // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
25392 // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
25393 // internal __substring_similar(str, pat, esc) call.
25394 if (first.eq_ignore_ascii_case("substring")
25395 || first.eq_ignore_ascii_case("substr"))
25396 && args.len() == 1
25397 && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
25398 {
25399 self.advance(); // SIMILAR
25400 let pattern = self.parse_expr(0)?;
25401 if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
25402 {
25403 return Err(self.err(format!(
25404 "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
25405 self.peek()
25406 )));
25407 }
25408 self.advance(); // ESCAPE
25409 let esc = self.parse_expr(0)?;
25410 if !matches!(self.peek(), Token::RParen) {
25411 return Err(self.err(format!(
25412 "expected ')' to close substring(... SIMILAR ...), got {:?}",
25413 self.peek()
25414 )));
25415 }
25416 self.advance();
25417 args.push(pattern);
25418 args.push(esc);
25419 return Ok(Expr::FunctionCall {
25420 name: "__substring_similar".to_string(),
25421 args,
25422 });
25423 }
25424 if (first.eq_ignore_ascii_case("substring")
25425 || first.eq_ignore_ascii_case("substr"))
25426 && args.len() == 1
25427 && matches!(self.peek(), Token::From | Token::For)
25428 {
25429 // `substring(str FROM pos [FOR len])`, or the FOR-only
25430 // `substring(str FOR len)` which PG treats as FROM 1.
25431 if matches!(self.peek(), Token::From) {
25432 self.advance();
25433 let start = self.parse_expr(0)?;
25434 args.push(start);
25435 } else {
25436 args.push(Expr::Literal(Literal::Integer(1)));
25437 }
25438 if matches!(self.peek(), Token::For) {
25439 self.advance();
25440 let length = self.parse_expr(0)?;
25441 args.push(length);
25442 }
25443 if !matches!(self.peek(), Token::RParen) {
25444 return Err(self.err(format!(
25445 "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
25446 self.peek()
25447 )));
25448 }
25449 self.advance();
25450 return Ok(Expr::FunctionCall {
25451 name: first.to_ascii_lowercase(),
25452 args,
25453 });
25454 }
25455 // PG `overlay(str PLACING repl FROM n [FOR len])`
25456 // syntactic form. Desugars to the `overlay(str,
25457 // repl, n[, len])` comma-list shape the evaluator
25458 // already implements. `PLACING` is not a reserved
25459 // token in SPG, so it arrives as a bare Ident.
25460 if first.eq_ignore_ascii_case("overlay")
25461 && args.len() == 1
25462 && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
25463 {
25464 self.advance(); // consume PLACING
25465 args.push(self.parse_expr(0)?); // replacement
25466 if !matches!(self.peek(), Token::From) {
25467 return Err(self.err(format!(
25468 "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
25469 self.peek()
25470 )));
25471 }
25472 self.advance();
25473 args.push(self.parse_expr(0)?); // start position
25474 if matches!(self.peek(), Token::For) {
25475 self.advance();
25476 args.push(self.parse_expr(0)?); // length
25477 }
25478 if !matches!(self.peek(), Token::RParen) {
25479 return Err(self.err(format!(
25480 "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
25481 self.peek()
25482 )));
25483 }
25484 self.advance();
25485 return Ok(Expr::FunctionCall {
25486 name: String::from("overlay"),
25487 args,
25488 });
25489 }
25490 // `TRIM(chars FROM str)` — the keyword-less
25491 // spelling lands here after the chars parse
25492 // (the keyword forms return earlier).
25493 if first.eq_ignore_ascii_case("trim")
25494 && args.len() == 1
25495 && matches!(self.peek(), Token::From)
25496 {
25497 self.advance();
25498 let target = self.parse_expr(0)?;
25499 if !matches!(self.peek(), Token::RParen) {
25500 return Err(self.err(format!(
25501 "expected ')' to close TRIM(chars FROM str), got {:?}",
25502 self.peek()
25503 )));
25504 }
25505 self.advance();
25506 let chars = args.pop().expect("one arg");
25507 return Ok(Expr::FunctionCall {
25508 name: String::from("btrim"),
25509 args: alloc::vec![target, chars],
25510 });
25511 }
25512 // v7.24 (round-16 A) — aggregate-internal
25513 // ordering: `array_agg(x ORDER BY y DESC NULLS
25514 // LAST)`. Keys close the argument list.
25515 if matches!(self.peek(), Token::Order) {
25516 self.advance();
25517 if !self.peek_is_by() {
25518 return Err(self.err(format!(
25519 "expected BY after ORDER in aggregate args, got {:?}",
25520 self.peek()
25521 )));
25522 }
25523 self.advance();
25524 loop {
25525 // v7.39 (round 691) — save/restore, the discipline this parser
25526 // already uses around `pending_sample_preds`, so a subquery inside
25527 // a key neither inherits nor leaks the channel.
25528 let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25529 let saved_coll = self.order_key_collation.take();
25530 let parsed = self.parse_expr(0);
25531 self.in_order_by_key = saved_flag;
25532 let collation =
25533 core::mem::replace(&mut self.order_key_collation, saved_coll);
25534 let expr = parsed?;
25535 let desc = if matches!(self.peek(), Token::Desc) {
25536 self.advance();
25537 true
25538 } else if matches!(self.peek(), Token::Asc) {
25539 self.advance();
25540 false
25541 } else {
25542 false
25543 };
25544 let nulls_first = self.parse_optional_nulls_placement()?;
25545 agg_order_by.push(OrderBy {
25546 expr,
25547 desc,
25548 nulls_first,
25549 collation,
25550 });
25551 if matches!(self.peek(), Token::Comma) {
25552 self.advance();
25553 } else {
25554 break;
25555 }
25556 }
25557 // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
25558 // follow the ORDER BY inside GROUP_CONCAT.
25559 if self.consume_group_concat_separator(&mut args)? {
25560 saw_separator = true;
25561 }
25562 if !matches!(self.peek(), Token::RParen) {
25563 return Err(self.err(format!(
25564 "expected ')' after aggregate ORDER BY, got {:?}",
25565 self.peek()
25566 )));
25567 }
25568 break;
25569 }
25570 // v7.39 (round 354, M12) — …or directly after the
25571 // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
25572 // own spelling of what PG passes as string_agg's second
25573 // argument; it was a parse error, so every MySQL query
25574 // that names its own separator failed outright.
25575 if self.consume_group_concat_separator(&mut args)? {
25576 saw_separator = true;
25577 break;
25578 }
25579 match self.peek() {
25580 Token::Comma => {
25581 self.advance();
25582 }
25583 Token::RParen => break,
25584 other => {
25585 return Err(self.err(format!(
25586 "expected ',' or ')' in function args, got {other:?}"
25587 )));
25588 }
25589 }
25590 }
25591 }
25592 // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
25593 // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
25594 // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
25595 // meaning a separator — that is what the explicit SEPARATOR
25596 // tail is for. Fold them into one `concat(...)` so the
25597 // aggregate keeps its single value argument.
25598 if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
25599 let values = args.len() - usize::from(saw_separator);
25600 if values > 1 {
25601 let sep_arg = if saw_separator { args.pop() } else { None };
25602 let folded = Expr::FunctionCall {
25603 name: "concat".to_string(),
25604 args: core::mem::take(&mut args),
25605 };
25606 args.push(folded);
25607 if let Some(sep) = sep_arg {
25608 args.push(sep);
25609 }
25610 }
25611 }
25612 self.advance(); // consume ')'
25613 // v7.39 (read01 round 77) — named arguments are NOT reordered here
25614 // any more. The parser has no catalog, so it could only ever resolve
25615 // the handful of `make_*` builtins whose parameter names were baked
25616 // into a table right here — every user function got
25617 // "does not support named arguments", though the catalog has been
25618 // storing its parameter names all along. Reordering happens in eval,
25619 // in one place, for builtins and user functions alike.
25620 // v7.32 (round-29) — ordered-set aggregate tail
25621 // `name(direct_args) WITHIN GROUP (ORDER BY …)`
25622 // (percentile_cont / percentile_disc / mode). The sort spec
25623 // lands in the same `order_by` slot a decorated aggregate
25624 // uses; the executor dispatches on the function name. WITHIN
25625 // GROUP and an intra-argument ORDER BY are mutually
25626 // exclusive (PG rejects both).
25627 let within_group_order = self.parse_within_group_clause()?;
25628 if !within_group_order.is_empty() && !agg_order_by.is_empty() {
25629 return Err(self.err(
25630 "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
25631 .into(),
25632 ));
25633 }
25634 let within_group_seen = !within_group_order.is_empty();
25635 let agg_order_by = if within_group_order.is_empty() {
25636 agg_order_by
25637 } else {
25638 within_group_order
25639 };
25640 // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
25641 let filter = self.parse_filter_clause()?;
25642 // v4.12: window-function tail — `name(args) OVER (...)`.
25643 // Promotes the just-parsed FunctionCall into a
25644 // WindowFunction node carrying partition + order.
25645 // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
25646 // / `RESPECT NULLS OVER (...)` between the closing paren
25647 // and `OVER`.
25648 let null_treatment = self.parse_null_treatment_modifier();
25649 if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25650 && s.eq_ignore_ascii_case("over")
25651 {
25652 self.advance();
25653 // v7.39 (round 230) — PG implements neither modifier for a
25654 // windowed call and says so (0A000). Both used to be parsed
25655 // and then silently dropped here, so `count(DISTINCT v)
25656 // OVER (…)` quietly answered the non-distinct count.
25657 if agg_distinct {
25658 return Err(
25659 self.err("DISTINCT is not implemented for window functions".to_string())
25660 );
25661 }
25662 if !agg_order_by.is_empty() {
25663 // PG separates the two shapes that land here: a
25664 // WITHIN GROUP call is an ordered-set aggregate and gets
25665 // its own message naming the aggregate; a plain
25666 // `agg(x ORDER BY y)` gets the generic one.
25667 let msg = if within_group_seen {
25668 alloc::format!("OVER is not supported for ordered-set aggregate {first}")
25669 } else {
25670 "aggregate ORDER BY is not implemented for window functions".to_string()
25671 };
25672 return Err(self.err(msg));
25673 }
25674 let (partition_by, order_by, frame) = self.parse_over_clause()?;
25675 return Ok(Expr::WindowFunction {
25676 name: first,
25677 args,
25678 partition_by,
25679 order_by,
25680 frame,
25681 null_treatment,
25682 filter,
25683 });
25684 }
25685 if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
25686 return Ok(Expr::AggregateOrdered {
25687 call: Box::new(Expr::FunctionCall { name: first, args }),
25688 order_by: agg_order_by,
25689 distinct: agg_distinct,
25690 filter,
25691 });
25692 }
25693 // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
25694 // over TIMESTAMPTZ and has no timestamp overload, so a
25695 // timestamp argument is coerced on the way in and the answer
25696 // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
25697 // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
25698 // zone`. SPG answered `timestamp without time zone`, dropping
25699 // the offset from every rendering.
25700 //
25701 // Writing the coercion PG performs makes the existing
25702 // argument-driven typing (the one `date_trunc` uses) reach the
25703 // right answer, rather than teaching the type layer a second
25704 // rule. MySQL's DATE_ADD is a different function that returns
25705 // DATE or DATETIME, so this is PG-dialect only.
25706 //
25707 // Out-of-line because this sits on the RECURSIVE descent
25708 // frame: an inline block with locals here costs every nesting
25709 // level, and the suite's deep-nesting sentinel overflowed the
25710 // 512 KiB parser stack the moment one was added (round 430's
25711 // lesson, in the same shape).
25712 if !self.mysql_dialect {
25713 lift_date_add_arg_to_timestamptz(&first, &mut args);
25714 }
25715 return Ok(Expr::FunctionCall { name: first, args });
25716 }
25717 // v7.9.20 — SQL-standard parenless keyword expressions
25718 // (PG treats these as functions called without parens).
25719 // Resolve to a synthetic FunctionCall so the engine's
25720 // eval path reuses the existing function-call routing.
25721 // mailrs G3.
25722 let lc = first.to_ascii_lowercase();
25723 if matches!(
25724 lc.as_str(),
25725 "current_date"
25726 | "current_time"
25727 | "current_timestamp"
25728 | "localtimestamp"
25729 | "localtime"
25730 // v7.37.17 (17.6 siblings) — session-identity SQL-
25731 // standard parenless keywords. current_user /
25732 // session_user / user were already caught by the
25733 // pgwire canned-response shortcut but bare-select
25734 // in the embedded engine went through Expr::Column
25735 // and errored. Adding them here so the parser
25736 // resolves to a synthetic FunctionCall that reuses
25737 // the existing eval/functions.rs dispatch.
25738 | "current_user"
25739 | "session_user"
25740 | "current_role"
25741 | "current_catalog"
25742 | "current_schema"
25743 | "current_database"
25744 // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
25745 | "system_user"
25746 ) {
25747 return Ok(Expr::FunctionCall {
25748 name: lc,
25749 args: Vec::new(),
25750 });
25751 }
25752 Ok(Expr::Column(ColumnName {
25753 qualifier: None,
25754 name: first,
25755 }))
25756 }
25757}
25758
25759/// v7.39 (round 522) — write the coercion PG's `date_add` /
25760/// `date_subtract` signature performs.
25761///
25762/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
25763/// timestamp argument is cast on the way in and the answer is
25764/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
25765/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
25766/// `timestamp without time zone`, dropping the offset from every
25767/// rendering of the result.
25768///
25769/// Writing the cast the signature implies lets the existing
25770/// argument-driven typing (the one `date_trunc` uses) reach the right
25771/// answer instead of teaching the type layer a second rule. MySQL's
25772/// DATE_ADD is a different function returning DATE or DATETIME, so the
25773/// caller applies this in PG dialect only.
25774///
25775/// A free function, and not a block at the call site, because the caller
25776/// is on the recursive-descent frame chain.
25777#[inline(never)]
25778fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
25779 if args.len() != 2
25780 || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
25781 {
25782 return;
25783 }
25784 let base = args.remove(0);
25785 args.insert(
25786 0,
25787 Expr::Cast {
25788 expr: Box::new(base),
25789 target: CastTarget::Timestamptz,
25790 },
25791 );
25792}
25793
25794/// v6.8.2 — walk an expression tree and return the first column
25795/// reference's bare name. Used by `parse_create_index_stmt_after_create`
25796/// to derive `CreateIndexStatement.column` from an expression
25797/// key (so downstream planner code resolving a primary column
25798/// position keeps working with expression indexes). Returns
25799/// `None` when the expression has no column ref at all — caller
25800/// surfaces that as a parse error.
25801fn extract_first_column(expr: &Expr) -> Option<String> {
25802 match expr {
25803 Expr::Column(cn) => Some(cn.name.clone()),
25804 Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
25805 Expr::Binary { lhs, rhs, .. } => {
25806 extract_first_column(lhs).or_else(|| extract_first_column(rhs))
25807 }
25808 Expr::Unary { expr: e, .. } => extract_first_column(e),
25809 // v7.39 (read01 round 93) — a cast wraps its operand: a common
25810 // expression-index key is `lower(col::text)`, where the column
25811 // sits under the `::text` cast inside the function arg. Without
25812 // descending here the key was rejected as "references no column".
25813 Expr::Cast { expr: e, .. } => extract_first_column(e),
25814 _ => None,
25815 }
25816}
25817
25818fn maybe_not(expr: Expr, negated: bool) -> Expr {
25819 if negated {
25820 Expr::Unary {
25821 op: UnOp::Not,
25822 expr: Box::new(expr),
25823 }
25824 } else {
25825 expr
25826 }
25827}
25828
25829/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
25830/// things in the two dialects, and SPG read all three PG's way:
25831///
25832/// | token | PG (and SPG) | MySQL, measured |
25833/// |---|---|---|
25834/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
25835/// | `&&` | inet / array overlap | **AND** |
25836/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
25837///
25838/// `1 || 0` answering the string '10' on a MySQL session is a wrong
25839/// answer with no error, which is why they are routed here rather than
25840/// left to the shared table.
25841impl Parser {
25842 fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
25843 if self.mysql_dialect {
25844 // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
25845 // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
25846 // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
25847 if let Token::Ident(w) = tok
25848 && w.eq_ignore_ascii_case("div")
25849 {
25850 return Some((BinOp::IntDiv, 8));
25851 }
25852 // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
25853 // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
25854 // the lexer; the `MOD(x, y)` function form is unaffected (MOD
25855 // there sits in operand position, not infix).
25856 if let Token::Ident(w) = tok
25857 && w.eq_ignore_ascii_case("mod")
25858 {
25859 return Some((BinOp::Mod, 8));
25860 }
25861 // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
25862 // plain ident to the lexer. Its precedence sits between OR (1)
25863 // and AND (3) — hence rung 2, the slot freed by moving AND up.
25864 if let Token::Ident(w) = tok
25865 && w.eq_ignore_ascii_case("xor")
25866 {
25867 return Some((BinOp::LogicalXor, 2));
25868 }
25869 match tok {
25870 Token::Concat => return Some((BinOp::Or, 1)),
25871 // MySQL's `&&` is logical AND, sharing AND's rung (3).
25872 Token::InetOverlap => return Some((BinOp::And, 3)),
25873 // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
25874 Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
25875 _ => {}
25876 }
25877 }
25878 binop_from(tok)
25879 }
25880}
25881
25882// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
25883// (which sits strictly between OR and AND), every level from AND upward was
25884// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
25885// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
25886// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
25887// the *relative* order of every PG operator is unchanged by the shift.
25888fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
25889 let pair = match tok {
25890 Token::Or => (BinOp::Or, 1),
25891 Token::And => (BinOp::And, 3),
25892 Token::Eq => (BinOp::Eq, 5),
25893 Token::NotEq => (BinOp::NotEq, 5),
25894 Token::Lt => (BinOp::Lt, 5),
25895 Token::LtEq => (BinOp::LtEq, 5),
25896 Token::Gt => (BinOp::Gt, 5),
25897 Token::GtEq => (BinOp::GtEq, 5),
25898 // pgvector distance ops all sit on the same rung — tighter than
25899 // comparisons (5) so `col <-> v < threshold` parses correctly.
25900 Token::L2Distance => (BinOp::L2Distance, 6),
25901 // v7.39 (read01 geo_ops.c) — geometric predicates ride the
25902 // comparison rung.
25903 Token::GeomParallel => (BinOp::GeomParallel, 5),
25904 // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
25905 // comparison rung.
25906 Token::OverLeft => (BinOp::OverLeft, 5),
25907 Token::OverRight => (BinOp::OverRight, 5),
25908 Token::GeomPerp => (BinOp::GeomPerp, 5),
25909 Token::GeomSameAs => (BinOp::GeomSameAs, 5),
25910 Token::ClosestPoint => (BinOp::ClosestPoint, 6),
25911 Token::GeomHoriz => (BinOp::GeomHoriz, 5),
25912 Token::InnerProduct => (BinOp::InnerProduct, 6),
25913 Token::CosineDistance => (BinOp::CosineDistance, 6),
25914 Token::Plus => (BinOp::Add, 7),
25915 Token::Minus => (BinOp::Sub, 7),
25916 // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
25917 // binds every "other" operator (`||`, `|`, `&`, `#`, the
25918 // pgvector distances above) BETWEEN additive (7) and the
25919 // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
25920 // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
25921 // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
25922 // ("matches PG conceptually" — the round-753 audit measured it
25923 // false; the old rung errored on `'a' || 1 + 1` with
25924 // `text + integer`). Same-level chains left-fold, as PG does.
25925 Token::Concat => (BinOp::Concat, 6),
25926 Token::Pipe => (BinOp::BitOr, 6),
25927 Token::Amp => (BinOp::BitAnd, 6),
25928 Token::Star => (BinOp::Mul, 8),
25929 Token::Slash => (BinOp::Div, 8),
25930 Token::Percent => (BinOp::Mod, 8),
25931 // v4.14: JSON path ops bind tighter than comparisons (5)
25932 // and additive (7) so `doc->'k' = 'v'` parses correctly.
25933 // Same rung as the multiplicative ops.
25934 Token::JsonGet => (BinOp::JsonGet, 8),
25935 Token::JsonGetText => (BinOp::JsonGetText, 8),
25936 Token::JsonGetPath => (BinOp::JsonGetPath, 8),
25937 Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
25938 Token::JsonContains => (BinOp::JsonContains, 8),
25939 Token::JsonPathExists => (BinOp::JsonPathExists, 8),
25940 Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
25941 Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
25942 Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
25943 Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
25944 Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
25945 // v7.12.2 — `@@` binds at the comparison rung (looser than
25946 // arithmetic, tighter than AND / OR). PG places `@@` at
25947 // the same precedence as `=` / `<`, so we follow.
25948 Token::TsMatch => (BinOp::TsMatch, 5),
25949 // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
25950 // PG places these at the comparison rung (same level as `=`),
25951 // so we follow.
25952 Token::InetContainedBy => (BinOp::InetContainedBy, 5),
25953 Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
25954 Token::InetContains => (BinOp::InetContains, 5),
25955 Token::InetContainsEq => (BinOp::InetContainsEq, 5),
25956 Token::InetOverlap => (BinOp::InetOverlap, 5),
25957 // v7.39 (round 508) — the geometric and pattern-order predicates
25958 // ride the comparison rung, as every other predicate does.
25959 Token::Intersects => (BinOp::Intersects, 5),
25960 Token::IsBelow => (BinOp::IsBelow, 5),
25961 Token::IsAbove => (BinOp::IsAbove, 5),
25962 Token::PatternLt => (BinOp::PatternLt, 5),
25963 Token::PatternLtEq => (BinOp::PatternLtEq, 5),
25964 Token::PatternGt => (BinOp::PatternGt, 5),
25965 Token::PatternGtEq => (BinOp::PatternGtEq, 5),
25966 // `@@@` is the old spelling of `@@` and means exactly it.
25967 Token::TsMatchOld => (BinOp::TsMatch, 5),
25968 _ => return None,
25969 };
25970 Some(pair)
25971}
25972
25973#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
25974// `as f32` here is intentional: vector elements widen / narrow into f32 on
25975// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
25976// past ~15 decimal digits — both are acceptable for a fixed-precision
25977// pgvector column.
25978/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
25979/// implicit table alias and break trailing clauses. WITH lands
25980/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
25981/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
25982/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
25983/// / VALUES / FOR / LATERAL — all of which would otherwise be
25984/// silently swallowed by `parse_optional_alias`.
25985fn is_alias_stopword(s: &str) -> bool {
25986 matches!(
25987 s.to_ascii_lowercase().as_str(),
25988 "with"
25989 | "on"
25990 | "where"
25991 | "having"
25992 | "group"
25993 | "order"
25994 | "limit"
25995 | "offset"
25996 | "union"
25997 | "except"
25998 | "intersect"
25999 | "returning"
26000 | "set"
26001 | "values"
26002 | "for"
26003 | "window"
26004 | "tablesample"
26005 | "lateral"
26006 | "left"
26007 | "right"
26008 | "inner"
26009 | "outer"
26010 | "full"
26011 | "cross"
26012 | "join"
26013 | "natural"
26014 | "using"
26015 | "fetch"
26016 )
26017}
26018
26019fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26020 match e {
26021 Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26022 Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26023 // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26024 // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26025 // so scale the divisor by hand instead of `f32::powi`.)
26026 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26027 let mut div = 1.0f32;
26028 for _ in 0..*scale {
26029 div *= 10.0;
26030 }
26031 Some(*unscaled as f32 / div)
26032 }
26033 Expr::Unary {
26034 op: UnOp::Neg,
26035 expr,
26036 } => extract_numeric_literal(expr).map(|x| -x),
26037 _ => None,
26038 }
26039}
26040
26041/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26042/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26043/// negative. Returns `None` if any pair fails to parse or no pair is found.
26044///
26045/// Recognised units (case-insensitive, optional trailing `s`):
26046/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26047/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26048/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26049/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26050/// (PG-canonical: DST and month-boundary semantics depend on this).
26051/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26052/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26053/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26054#[allow(clippy::cast_possible_truncation)]
26055fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26056 let mut months: i64 = 0;
26057 let mut days: i64 = 0;
26058 let mut micros: i64 = 0;
26059 let mut in_time = false;
26060 let mut num = alloc::string::String::new();
26061 for ch in rest.chars() {
26062 if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26063 num.push(ch);
26064 continue;
26065 }
26066 if ch == 'T' || ch == 't' {
26067 if !num.is_empty() {
26068 return None;
26069 }
26070 in_time = true;
26071 continue;
26072 }
26073 let n: f64 = num.parse().ok()?;
26074 num.clear();
26075 match (ch, in_time) {
26076 ('Y' | 'y', false) => months += (n * 12.0) as i64,
26077 ('M', false) => months += n as i64,
26078 ('W' | 'w', false) => days += (n * 7.0) as i64,
26079 ('D' | 'd', false) => days += n as i64,
26080 ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26081 ('M', true) => micros += (n * 60_000_000.0) as i64,
26082 ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26083 _ => return None,
26084 }
26085 }
26086 if !num.is_empty() {
26087 return None;
26088 }
26089 Some((
26090 i32::try_from(months).ok()?,
26091 i32::try_from(days).ok()?,
26092 micros,
26093 ))
26094}
26095
26096/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
26097/// leading `-` negates the whole value). Rejects date-like strings.
26098fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
26099 let (neg, body) = match s.strip_prefix('-') {
26100 Some(b) => (true, b),
26101 None => (false, s),
26102 };
26103 let (y, m) = body.split_once('-')?;
26104 let years: i32 = y.parse().ok()?;
26105 let mons: i32 = m.parse().ok()?;
26106 if years < 0 || mons < 0 {
26107 return None;
26108 }
26109 let total = years.checked_mul(12)?.checked_add(mons)?;
26110 Some((if neg { -total } else { total }, 0, 0))
26111}
26112
26113/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
26114/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
26115fn parse_interval_clock(tok: &str) -> Option<i64> {
26116 let (neg, body) = match tok.strip_prefix('-') {
26117 Some(r) => (true, r),
26118 None => (false, tok.strip_prefix('+').unwrap_or(tok)),
26119 };
26120 let mut it = body.split(':');
26121 let h: i64 = it.next()?.parse().ok()?;
26122 let m: i64 = it.next()?.parse().ok()?;
26123 let s_tok = it.next().unwrap_or("0");
26124 if it.next().is_some() {
26125 return None;
26126 }
26127 let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
26128 let sec: i64 = sec.parse().ok()?;
26129 let mut f = alloc::string::String::from(frac);
26130 while f.len() < 6 {
26131 f.push('0');
26132 }
26133 f.truncate(6);
26134 let fus: i64 = f.parse().ok()?;
26135 sec.checked_mul(1_000_000)?.checked_add(fus)?
26136 } else {
26137 s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
26138 };
26139 let total = h
26140 .checked_mul(3_600_000_000)?
26141 .checked_add(m.checked_mul(60_000_000)?)?
26142 .checked_add(sec_us)?;
26143 Some(if neg { -total } else { total })
26144}
26145
26146/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
26147/// every spelling PG accepts (measured against live PG18.4, not guessed):
26148/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
26149/// Before this, the unit table matched long names only, with an ad-hoc
26150/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
26151/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
26152/// INTERVAL", and it had also grown arms for the debris that stripping leaves
26153/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
26154/// fractional) both read from this one table now.
26155fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
26156 let u = raw.to_ascii_lowercase();
26157 Some(match u.as_str() {
26158 "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
26159 "microsecond"
26160 }
26161 "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
26162 "millisecond"
26163 }
26164 "second" | "seconds" | "sec" | "secs" | "s" => "second",
26165 "minute" | "minutes" | "min" | "mins" | "m" => "minute",
26166 "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
26167 "day" | "days" | "d" => "day",
26168 "week" | "weeks" | "w" => "week",
26169 "month" | "months" | "mon" | "mons" => "month",
26170 "year" | "years" | "yr" | "yrs" | "y" => "year",
26171 "decade" | "decades" | "dec" | "decs" => "decade",
26172 "century" | "centuries" | "cent" | "c" => "century",
26173 "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
26174 _ => return None,
26175 })
26176}
26177
26178/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
26179/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
26180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26181pub(crate) enum IntervalField {
26182 Year,
26183 Month,
26184 Day,
26185 Hour,
26186 Minute,
26187 Second,
26188}
26189
26190/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
26191/// spellings aren't standard for the qualifier position, so only the singular
26192/// forms are accepted.
26193/// v7.39 (round 350, M7) — MySQL's interval units, measured against
26194/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
26195/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
26196/// take a `'1 2'` style literal — are not read here; they stay a parse
26197/// error rather than being silently misread.)
26198/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
26199///
26200/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
26201/// to do with a `@@` engine setting, and an unset one reads NULL rather
26202/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
26203/// were the same node and `SELECT @x` answered "Unknown system variable".)
26204/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
26205/// not see a session override — measured, after `SET autocommit=0`,
26206/// `@@global.autocommit` is still 1.
26207///
26208/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
26209/// the parser's nesting budget is tuned against, and building these
26210/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
26211/// wall `parse_left_right_atom` and friends were factored out for).
26212#[inline(never)]
26213fn variable_ref_atom(raw: &str) -> Expr {
26214 let user_var = !raw.starts_with("@@");
26215 let bare = raw.trim_start_matches('@').to_ascii_lowercase();
26216 Expr::FunctionCall {
26217 name: String::from(if user_var {
26218 "__spg_user_var"
26219 } else {
26220 "__spg_session_var"
26221 }),
26222 args: alloc::vec![Expr::Literal(Literal::String(bare))],
26223 }
26224}
26225
26226fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
26227 let Token::Ident(s) = tok else { return None };
26228 Some(match () {
26229 () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
26230 () if s.eq_ignore_ascii_case("second") => "second",
26231 () if s.eq_ignore_ascii_case("minute") => "minute",
26232 () if s.eq_ignore_ascii_case("hour") => "hour",
26233 () if s.eq_ignore_ascii_case("day") => "day",
26234 () if s.eq_ignore_ascii_case("week") => "week",
26235 () if s.eq_ignore_ascii_case("month") => "month",
26236 () if s.eq_ignore_ascii_case("quarter") => "quarter",
26237 () if s.eq_ignore_ascii_case("year") => "year",
26238 () => return None,
26239 })
26240}
26241
26242/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
26243/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
26244/// which constructs the value at run time. Only the slot the unit names
26245/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
26246/// slot the builtin has (months and fractional seconds respectively).
26247fn make_interval_call(qty: Expr, unit: &str) -> Expr {
26248 let zero = || Expr::Literal(Literal::Integer(0));
26249 let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
26250 lhs: alloc::boxed::Box::new(qty.clone()),
26251 op,
26252 rhs: alloc::boxed::Box::new(by),
26253 };
26254 // (years, months, weeks, days, hours, mins, secs)
26255 let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
26256 match unit {
26257 "year" => args[0] = qty,
26258 "quarter" => {
26259 args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
26260 }
26261 "month" => args[1] = qty,
26262 "week" => args[2] = qty,
26263 "day" => args[3] = qty,
26264 "hour" => args[4] = qty,
26265 "minute" => args[5] = qty,
26266 "second" => args[6] = qty,
26267 // The builtin's seconds slot takes a fraction, so microseconds ride
26268 // it scaled down; the divisor is a NUMERIC literal so the division
26269 // stays exact rather than going through a float.
26270 "microsecond" => {
26271 args[6] = scaled(
26272 crate::ast::BinOp::Div,
26273 Expr::Literal(Literal::Numeric {
26274 unscaled: 1_000_000,
26275 scale: 0,
26276 }),
26277 );
26278 }
26279 _ => args[3] = qty,
26280 }
26281 Expr::FunctionCall {
26282 name: alloc::string::String::from("make_interval"),
26283 args,
26284 }
26285}
26286
26287/// `(count, unit)` → `(months, days, micros)`.
26288fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
26289 let n: i64 = count.trim().parse().ok()?;
26290 Some(match unit {
26291 "microsecond" => (0, 0, n),
26292 "second" => (0, 0, n.checked_mul(1_000_000)?),
26293 "minute" => (0, 0, n.checked_mul(60_000_000)?),
26294 "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
26295 "day" => (0, i32::try_from(n).ok()?, 0),
26296 "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
26297 "month" => (i32::try_from(n).ok()?, 0, 0),
26298 "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
26299 "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
26300 _ => return None,
26301 })
26302}
26303
26304fn interval_field_of(tok: &Token) -> Option<IntervalField> {
26305 let Token::Ident(s) = tok else { return None };
26306 Some(match () {
26307 () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
26308 () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
26309 () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
26310 () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
26311 () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
26312 () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
26313 () => return None,
26314 })
26315}
26316
26317/// v7.39 (read01 round 102) — interpret an interval literal under a field
26318/// qualifier. Returns `(months, days, micros)`.
26319///
26320/// * A single field applied to a bare number sets which unit the number means,
26321/// truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
26322/// SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
26323/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
26324/// * Every other range, and any literal a single field can't read as a plain
26325/// number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
26326/// interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
26327/// like PG, and the qualifier there only bounds precision.
26328fn interpret_qualified_interval(
26329 text: &str,
26330 (f1, f2): (IntervalField, Option<IntervalField>),
26331) -> Option<(i32, i32, i64)> {
26332 if let Some(f2) = f2 {
26333 if f1 == IntervalField::Year && f2 == IntervalField::Month {
26334 if let Some(m) = parse_year_month_literal(text) {
26335 return Some((m, 0, 0));
26336 }
26337 }
26338 return parse_interval_text(text);
26339 }
26340 // Single field: reinterpret a bare number; otherwise the default parse.
26341 let trimmed = text.trim();
26342 if let Ok(val) = trimmed.parse::<f64>() {
26343 // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
26344 #[allow(clippy::cast_possible_truncation)]
26345 let whole = val as i64;
26346 #[allow(clippy::cast_possible_truncation)]
26347 let secs_micros = {
26348 let m = val * 1_000_000.0;
26349 if m >= 0.0 {
26350 (m + 0.5) as i64
26351 } else {
26352 (m - 0.5) as i64
26353 }
26354 };
26355 return Some(match f1 {
26356 IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
26357 IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
26358 IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
26359 IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
26360 IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
26361 IntervalField::Second => (0, 0, secs_micros),
26362 });
26363 }
26364 parse_interval_text(text)
26365}
26366
26367/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
26368fn parse_year_month_literal(text: &str) -> Option<i32> {
26369 let t = text.trim();
26370 let (neg, body) = match t.strip_prefix('-') {
26371 Some(r) => (true, r),
26372 None => (false, t.strip_prefix('+').unwrap_or(t)),
26373 };
26374 let mut it = body.split('-');
26375 let years: i32 = it.next()?.trim().parse().ok()?;
26376 let months: i32 = match it.next() {
26377 Some(m) => m.trim().parse().ok()?,
26378 None => 0,
26379 };
26380 if it.next().is_some() {
26381 return None;
26382 }
26383 let total = years.checked_mul(12)?.checked_add(months)?;
26384 Some(if neg { -total } else { total })
26385}
26386
26387pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
26388 // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
26389 // `@` is decorative; a trailing `ago` negates the whole interval.
26390 let mut trimmed = s.trim();
26391 trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
26392 let mut negate = false;
26393 if let Some(rest) = trimmed
26394 .strip_suffix("ago")
26395 .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
26396 {
26397 negate = true;
26398 trimmed = rest.trim();
26399 }
26400 let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
26401 let (mo, d, us) = v?;
26402 if negate {
26403 Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
26404 } else {
26405 Some((mo, d, us))
26406 }
26407 };
26408 let s = trimmed;
26409 // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
26410 // are single tokens, not the `<n> <unit>` pair form handled below.
26411 if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
26412 return finish(parse_iso8601_interval(rest));
26413 }
26414 if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
26415 if let Some(iv) = parse_year_month_interval(trimmed) {
26416 return finish(Some(iv));
26417 }
26418 }
26419 // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
26420 // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
26421 // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
26422 if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
26423 if let Ok(n) = trimmed.parse::<i64>() {
26424 return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
26425 }
26426 if let Ok(f) = trimmed.parse::<f64>() {
26427 if f.is_finite() {
26428 #[allow(clippy::cast_possible_truncation)]
26429 return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
26430 }
26431 }
26432 }
26433 // v7.39 (round 243) — PG accepts the number and unit run together
26434 // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
26435 // the `<n> <unit>` pair loop below sees them as two.
26436 let raw_parts: Vec<&str> = s.split_whitespace().collect();
26437 let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
26438 for p in raw_parts {
26439 let boundary = p
26440 .char_indices()
26441 .find(|(i, c)| {
26442 *i > 0
26443 && c.is_ascii_alphabetic()
26444 && p[..*i]
26445 .chars()
26446 .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
26447 && p[..*i].chars().any(|d| d.is_ascii_digit())
26448 })
26449 .map(|(i, _)| i);
26450 match boundary {
26451 Some(i) => {
26452 parts.push(&p[..i]);
26453 parts.push(&p[i..]);
26454 }
26455 None => parts.push(p),
26456 }
26457 }
26458 // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
26459 // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
26460 // remains is the `<n> <unit>` pair form handled below.
26461 let mut clock_us: i64 = 0;
26462 let mut had_clock = false;
26463 if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
26464 clock_us = parse_interval_clock(parts[pos])?;
26465 parts.remove(pos);
26466 had_clock = true;
26467 }
26468 // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
26469 // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
26470 let mut lone_days: i32 = 0;
26471 if had_clock && parts.len() == 1 {
26472 if let Ok(n) = parts[0].parse::<i64>() {
26473 lone_days = i32::try_from(n).ok()?;
26474 parts.clear();
26475 }
26476 }
26477 if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
26478 return None;
26479 }
26480 let mut months: i32 = 0;
26481 let mut days: i32 = lone_days;
26482 let mut micros: i64 = clock_us;
26483 let mut i = 0;
26484 while i < parts.len() {
26485 let unit_stripped = canonical_interval_unit(parts[i + 1])?;
26486 if let Ok(n) = parts[i].parse::<i64>() {
26487 match unit_stripped {
26488 "microsecond" => micros = micros.checked_add(n)?,
26489 "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
26490 "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
26491 "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
26492 "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
26493 "day" => {
26494 let n32 = i32::try_from(n).ok()?;
26495 days = days.checked_add(n32)?;
26496 }
26497 "week" => {
26498 let n32 = i32::try_from(n).ok()?;
26499 days = days.checked_add(n32.checked_mul(7)?)?;
26500 }
26501 "month" => {
26502 let n32 = i32::try_from(n).ok()?;
26503 months = months.checked_add(n32)?;
26504 }
26505 "year" => {
26506 let n32 = i32::try_from(n).ok()?;
26507 months = months.checked_add(n32.checked_mul(12)?)?;
26508 }
26509 // v7.39 (read01 timestamp.c) — the larger calendar units.
26510 "decade" => {
26511 let n32 = i32::try_from(n).ok()?;
26512 months = months.checked_add(n32.checked_mul(120)?)?;
26513 }
26514 "century" => {
26515 let n32 = i32::try_from(n).ok()?;
26516 months = months.checked_add(n32.checked_mul(1200)?)?;
26517 }
26518 "millennium" => {
26519 let n32 = i32::try_from(n).ok()?;
26520 months = months.checked_add(n32.checked_mul(12000)?)?;
26521 }
26522 _ => return None,
26523 }
26524 } else if let Ok(f) = parts[i].parse::<f64>() {
26525 // Fractional units cascade down to the next-finer field the way
26526 // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
26527 // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
26528 // no_std: f64 has no trunc/fract/round methods, so do them with
26529 // casts (toward-zero) + explicit round-half-away-from-zero.
26530 #[allow(clippy::cast_possible_truncation)]
26531 fn round_i64(x: f64) -> i64 {
26532 if x >= 0.0 {
26533 (x + 0.5) as i64
26534 } else {
26535 (x - 0.5) as i64
26536 }
26537 }
26538 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26539 fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
26540 const DAY_US: f64 = 86_400_000_000.0;
26541 let whole = d as i64; // truncates toward zero
26542 let frac = d - whole as f64;
26543 *days = days.checked_add(i32::try_from(whole).ok()?)?;
26544 *micros = micros.checked_add(round_i64(frac * DAY_US))?;
26545 Some(())
26546 }
26547 #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26548 match unit_stripped {
26549 "microsecond" => micros = micros.checked_add(round_i64(f))?,
26550 "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
26551 "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
26552 "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
26553 "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
26554 "day" => add_days_frac(&mut days, &mut micros, f)?,
26555 "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
26556 "month" => {
26557 let whole = f as i64;
26558 months = months.checked_add(i32::try_from(whole).ok()?)?;
26559 add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
26560 }
26561 "year" => {
26562 let m = f * 12.0;
26563 let whole = m as i64;
26564 months = months.checked_add(i32::try_from(whole).ok()?)?;
26565 add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
26566 }
26567 _ => return None,
26568 }
26569 } else {
26570 return None;
26571 }
26572 i += 2;
26573 }
26574 finish(Some((months, days, micros)))
26575}
26576
26577/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
26578/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
26579/// `interval` is intentionally absent (handled by its own parser arm).
26580/// Returns `None` for names that aren't sensible as a bare typed literal, so
26581/// the caller falls back to treating the ident as a column reference.
26582fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
26583 Some(match ident {
26584 "date" => CastTarget::Date,
26585 "timestamp" | "datetime" => CastTarget::Timestamp,
26586 "timestamptz" => CastTarget::Timestamptz,
26587 "bool" | "boolean" => CastTarget::Bool,
26588 "int" | "integer" | "int4" => CastTarget::Int,
26589 "bigint" | "int8" => CastTarget::BigInt,
26590 "float8" | "double precision" => CastTarget::Float,
26591 "uuid" => CastTarget::Uuid,
26592 "bytea" => CastTarget::Bytea,
26593 "json" => CastTarget::Json,
26594 "jsonb" => CastTarget::Jsonb,
26595 // Types without a dedicated CastTarget variant flow through the
26596 // generic Named path (engine resolves via column_type_to_data_type).
26597 "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
26598 | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
26599 | "money" | "bit" | "varbit"
26600 // Geometric types accept the `TYPE 'literal'` prefix spelling too.
26601 | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
26602 // Range / multirange types likewise.
26603 | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
26604 | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
26605 | "datemultirange" | "tsmultirange" | "tstzmultirange"
26606 // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
26607 | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
26608 CastTarget::Named(alloc::string::String::from(ident))
26609 }
26610 _ => return None,
26611 })
26612}
26613
26614/// v7.12.4 — map a bare type-name identifier (the form that
26615/// appears in a function arg list or RETURNS clause) to a
26616/// [`ColumnTypeName`]. Returns `None` for unknown / extension
26617/// types so the caller can preserve them as
26618/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
26619///
26620/// Subset of the full column-type grammar — we deliberately
26621/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
26622/// here because function-arg types in v7.12.4 are mostly the
26623/// bare form (`text`, `int`, `bytea`, …).
26624/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
26625/// than being `name TYPE`?
26626///
26627/// The multi-word spellings SQL allows for a bare argument type, each
26628/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
26629///
26630/// NOTE this list also exists in `spg-storage`, which computes the
26631/// signature key from the rendered argument text and has to reach the
26632/// same verdict. The two crates are siblings — neither depends on the
26633/// other — and each already carries its own table of type spellings
26634/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
26635/// there), so this follows the structure rather than inventing new
26636/// duplication. Recorded as V49.
26637pub fn is_multiword_type_phrase(phrase: &str) -> bool {
26638 let t = phrase.trim().to_ascii_lowercase();
26639 let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
26640 matches!(
26641 base,
26642 "double precision"
26643 | "character varying"
26644 | "bit varying"
26645 | "timestamp with time zone"
26646 | "timestamp without time zone"
26647 | "time with time zone"
26648 | "time without time zone"
26649 | "national character"
26650 | "national character varying"
26651 )
26652}
26653
26654fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
26655 Some(match ident.to_ascii_lowercase().as_str() {
26656 "smallint" | "tinyint" => ColumnTypeName::SmallInt,
26657 "int" | "integer" | "mediumint" => ColumnTypeName::Int,
26658 "bigint" => ColumnTypeName::BigInt,
26659 "float" | "double" => ColumnTypeName::Float,
26660 // v7.39 (round 269) — real is 32-bit.
26661 "real" | "float4" => ColumnTypeName::Real,
26662 "text" => ColumnTypeName::Text,
26663 "bool" | "boolean" => ColumnTypeName::Bool,
26664 "date" => ColumnTypeName::Date,
26665 "timestamp" | "datetime" => ColumnTypeName::Timestamp,
26666 "timestamptz" => ColumnTypeName::Timestamptz,
26667 "json" => ColumnTypeName::Json,
26668 "jsonb" => ColumnTypeName::Jsonb,
26669 "bytea" | "bytes" => ColumnTypeName::Bytes,
26670 "tsvector" => ColumnTypeName::TsVector,
26671 "tsquery" => ColumnTypeName::TsQuery,
26672 "uuid" => ColumnTypeName::Uuid,
26673 "interval" => ColumnTypeName::Interval,
26674 "time" => ColumnTypeName::Time,
26675 "year" => ColumnTypeName::Year,
26676 "timetz" => ColumnTypeName::TimeTz,
26677 "money" => ColumnTypeName::Money,
26678 _ => return None,
26679 })
26680}
26681
26682/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
26683/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
26684///
26685/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
26686/// / embedded SQL land in v7.12.5+):
26687///
26688/// ```text
26689/// body := [ws] block [ws]
26690/// block := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
26691/// stmt := assign | return
26692/// assign := assign_target := expr
26693/// assign_target := ( NEW | OLD ) . ident | ident
26694/// return := RETURN ( NEW | OLD | NULL | expr )
26695/// ```
26696///
26697/// `expr` is parsed by recursing into the regular `Parser` — so a
26698/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
26699/// NEW.subject || ' ' || NEW.sender)` body shape works without
26700/// the body parser knowing what `to_tsvector` is.
26701///
26702/// Errors here cause the caller to fall back to
26703/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
26704/// successful, but the executor will refuse to invoke the
26705/// function with an "unparseable body" error.
26706/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
26707/// from the crate root as `spg_sql::parse_function_body`.
26708pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26709 parse_plpgsql_body(body)
26710}
26711
26712fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
26713 // Use the regular lexer on the body text. The trailing
26714 // `END;` may or may not have a semicolon; the lexer treats
26715 // both forms identically.
26716 let tokens = lexer::tokenize(body).map_err(|e| ParseError {
26717 message: alloc::format!("plpgsql body lex error: {e}"),
26718 token_pos: 0,
26719 })?;
26720 let mut parser = Parser::new(tokens);
26721 parser.parse_plpgsql_block()
26722}
26723
26724/// v7.39 (GUC) — the textual body of a SET value, for list joining.
26725fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
26726 match v {
26727 crate::ast::SetValue::String(s)
26728 | crate::ast::SetValue::Ident(s)
26729 | crate::ast::SetValue::Number(s) => s.clone(),
26730 crate::ast::SetValue::Default => "DEFAULT".into(),
26731 }
26732}
26733
26734/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
26735/// contains an aggregate call at ITS OWN query level (recursion stops at
26736/// sublink boundaries — a sublink's aggregates belong to the sublink).
26737/// Backs the "aggregate functions are not allowed in a recursive query's
26738/// recursive term" well-formedness check.
26739fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
26740 const AGG_NAMES: &[&str] = &[
26741 "count",
26742 "sum",
26743 "min",
26744 "max",
26745 "avg",
26746 "string_agg",
26747 "array_agg",
26748 "bool_and",
26749 "bool_or",
26750 "every",
26751 "any_value",
26752 "json_agg",
26753 "jsonb_agg",
26754 "json_object_agg",
26755 "jsonb_object_agg",
26756 "bit_and",
26757 "bit_or",
26758 "bit_xor",
26759 "var_pop",
26760 "var_samp",
26761 "variance",
26762 "stddev",
26763 "stddev_pop",
26764 "stddev_samp",
26765 "range_agg",
26766 "range_intersect_agg",
26767 "percentile_cont",
26768 "percentile_disc",
26769 "mode",
26770 "corr",
26771 "covar_pop",
26772 "covar_samp",
26773 ];
26774 match e {
26775 Expr::AggregateOrdered { .. } => true,
26776 Expr::FunctionCall { name, args } => {
26777 AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
26778 || args.iter().any(expr_has_toplevel_aggregate)
26779 }
26780 Expr::NamedArg { expr, .. }
26781 | Expr::Variadic(expr)
26782 | Expr::Unary { expr, .. }
26783 | Expr::Cast { expr, .. }
26784 | Expr::IsNull { expr, .. }
26785 | Expr::FieldAccess { base: expr, .. }
26786 | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
26787 Expr::Binary { lhs, rhs, .. } => {
26788 expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
26789 }
26790 Expr::Like { expr, pattern, .. } => {
26791 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
26792 }
26793 Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
26794 Expr::InList { expr, list, .. } => {
26795 expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
26796 }
26797 Expr::ArraySubscript { target, index } => {
26798 expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
26799 }
26800 Expr::ArraySlice { target, lo, hi } => {
26801 expr_has_toplevel_aggregate(target)
26802 || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
26803 || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
26804 }
26805 Expr::AnyAll { expr, array, .. } => {
26806 expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
26807 }
26808 Expr::Case {
26809 operand,
26810 branches,
26811 else_branch,
26812 } => {
26813 operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
26814 || branches
26815 .iter()
26816 .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
26817 || else_branch
26818 .as_deref()
26819 .is_some_and(expr_has_toplevel_aggregate)
26820 }
26821 // The outer-level operands of a sublink can aggregate; the sublink's
26822 // own body cannot leak its aggregates up here.
26823 Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
26824 Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
26825 row.iter().any(expr_has_toplevel_aggregate)
26826 }
26827 _ => false,
26828 }
26829}
26830
26831/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
26832/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
26833/// named table anywhere in its subtree. A plain FROM derived table is NOT a
26834/// sublink and is legal in a recursive term, so it is not walked here.
26835fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
26836 let mut exprs: Vec<&Expr> = Vec::new();
26837 for it in &s.items {
26838 if let crate::ast::SelectItem::Expr { expr, .. } = it {
26839 exprs.push(expr);
26840 }
26841 }
26842 if let Some(w) = &s.where_ {
26843 exprs.push(w);
26844 }
26845 if let Some(h) = &s.having {
26846 exprs.push(h);
26847 }
26848 if let Some(g) = &s.group_by {
26849 exprs.extend(g.iter());
26850 }
26851 if let Some(from) = &s.from {
26852 for j in &from.joins {
26853 if let Some(on) = &j.on {
26854 exprs.push(on);
26855 }
26856 }
26857 }
26858 exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
26859}
26860
26861/// Does this expression contain a sublink whose subquery mentions `name`?
26862fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
26863 match e {
26864 Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
26865 Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
26866 Expr::InSubquery { expr, subquery, .. } => {
26867 expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
26868 }
26869 Expr::RowInSubquery { row, subquery, .. } => {
26870 row.iter().any(|x| expr_sublink_mentions(x, name))
26871 || select_mentions_table(subquery, name)
26872 }
26873 Expr::RowCmpSubquery { row, subquery, .. } => {
26874 row.iter().any(|x| expr_sublink_mentions(x, name))
26875 || select_mentions_table(subquery, name)
26876 }
26877 Expr::NamedArg { expr, .. }
26878 | Expr::Variadic(expr)
26879 | Expr::Unary { expr, .. }
26880 | Expr::Cast { expr, .. }
26881 | Expr::IsNull { expr, .. }
26882 | Expr::FieldAccess { base: expr, .. }
26883 | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
26884 Expr::Binary { lhs, rhs, .. } => {
26885 expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
26886 }
26887 Expr::Like { expr, pattern, .. } => {
26888 expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
26889 }
26890 Expr::FunctionCall { args, .. } | Expr::Array(args) => {
26891 args.iter().any(|x| expr_sublink_mentions(x, name))
26892 }
26893 Expr::InList { expr, list, .. } => {
26894 expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
26895 }
26896 Expr::ArraySubscript { target, index } => {
26897 expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
26898 }
26899 Expr::ArraySlice { target, lo, hi } => {
26900 expr_sublink_mentions(target, name)
26901 || lo
26902 .as_deref()
26903 .is_some_and(|x| expr_sublink_mentions(x, name))
26904 || hi
26905 .as_deref()
26906 .is_some_and(|x| expr_sublink_mentions(x, name))
26907 }
26908 Expr::AnyAll { expr, array, .. } => {
26909 expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
26910 }
26911 Expr::Case {
26912 operand,
26913 branches,
26914 else_branch,
26915 } => {
26916 operand
26917 .as_deref()
26918 .is_some_and(|x| expr_sublink_mentions(x, name))
26919 || branches
26920 .iter()
26921 .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
26922 || else_branch
26923 .as_deref()
26924 .is_some_and(|x| expr_sublink_mentions(x, name))
26925 }
26926 _ => false,
26927 }
26928}
26929
26930/// Does this SELECT (in full — FROM tables, derived tables, its own
26931/// sublinks, and union arms) mention the named table?
26932fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
26933 if let Some(from) = &s.from {
26934 if from.primary.name.eq_ignore_ascii_case(name) {
26935 return true;
26936 }
26937 if let Some(sub) = &from.primary.lateral_subquery
26938 && select_mentions_table(sub, name)
26939 {
26940 return true;
26941 }
26942 for j in &from.joins {
26943 if j.table.name.eq_ignore_ascii_case(name) {
26944 return true;
26945 }
26946 if let Some(sub) = &j.table.lateral_subquery
26947 && select_mentions_table(sub, name)
26948 {
26949 return true;
26950 }
26951 }
26952 }
26953 if select_has_self_ref_in_sublink(s, name) {
26954 return true;
26955 }
26956 s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
26957}
26958
26959/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
26960/// row count, the way PG evaluates one before applying it.
26961///
26962/// `None` = not a constant (a column, a subquery, a function call).
26963/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
26964/// message stands in for LIMIT / OFFSET, which the caller substitutes.
26965/// All wordings were read off live PG 18.4.
26966fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
26967 use crate::ast::{BinOp, Expr, Literal, UnOp};
26968 match e {
26969 Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
26970 Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26971 Some(Ok(round_scaled_half_away(*unscaled, *scale)))
26972 }
26973 // PG coerces a string by its CONTENT, and fails on the value.
26974 Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
26975 |_| {
26976 Err(alloc::format!(
26977 "invalid input syntax for type bigint: \"{t}\""
26978 ))
26979 },
26980 |n| Ok(i128::from(n)),
26981 )),
26982 Expr::Literal(Literal::Bool(_)) => Some(Err(
26983 "argument of {L} must be type bigint, not type boolean".into(),
26984 )),
26985 Expr::Unary {
26986 op: UnOp::Neg,
26987 expr,
26988 } => match fold_limit_constant(expr)? {
26989 Ok(v) => Some(Ok(-v)),
26990 e @ Err(_) => Some(e),
26991 },
26992 Expr::Binary { lhs, op, rhs } => {
26993 let a = match fold_limit_constant(lhs)? {
26994 Ok(v) => v,
26995 e @ Err(_) => return Some(e),
26996 };
26997 let b = match fold_limit_constant(rhs)? {
26998 Ok(v) => v,
26999 e @ Err(_) => return Some(e),
27000 };
27001 let out = match op {
27002 BinOp::Add => a.checked_add(b),
27003 BinOp::Sub => a.checked_sub(b),
27004 BinOp::Mul => a.checked_mul(b),
27005 BinOp::Div if b != 0 => a.checked_div(b),
27006 BinOp::Div => return Some(Err("division by zero".into())),
27007 BinOp::Mod if b != 0 => a.checked_rem(b),
27008 BinOp::Mod => return Some(Err("division by zero".into())),
27009 _ => return None,
27010 };
27011 // PG evaluates the arithmetic in the operand's own type, so an
27012 // int-by-int product that leaves int range fails there — before
27013 // the row count is ever looked at.
27014 match out {
27015 Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27016 Some(Err("integer out of range".into()))
27017 }
27018 Some(v) => Some(Ok(v)),
27019 None => Some(Err("integer out of range".into())),
27020 }
27021 }
27022 _ => None,
27023 }
27024}
27025
27026/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27027/// cast, which is what makes `LIMIT 2.5` keep three rows.
27028fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27029 if scale == 0 {
27030 return unscaled;
27031 }
27032 let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27033 return 0;
27034 };
27035 let neg = unscaled < 0;
27036 let mag = unscaled.unsigned_abs() as i128;
27037 let rounded = (mag + div / 2) / div;
27038 if neg { -rounded } else { rounded }
27039}
27040
27041#[cfg(test)]
27042mod tests {
27043 use super::*;
27044 use alloc::string::ToString;
27045
27046 fn parse(s: &str) -> Statement {
27047 parse_statement(s).expect("parse ok")
27048 }
27049
27050 // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27051 // `tables`, `partition`, etc. are unreserved keywords per PG's
27052 // `pg_get_keywords()` and MUST be usable as column / table /
27053 // alias names. Pre-T4 every drop-in user whose schema had one
27054 // of these as a column name (sentori events.release, mailrs
27055 // messages.index in some forks) blew the parser up at CREATE
27056 // TABLE time with "expected identifier, got Release". The
27057 // generalisation lives in `unreserved_keyword_text` + the
27058 // `expect_ident_like` and `parse_atom` arms that consult it.
27059 #[test]
27060 fn release_usable_as_column_name_in_create_table() {
27061 let stmt =
27062 parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
27063 if let Statement::CreateTable(t) = stmt {
27064 let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
27065 assert_eq!(names, alloc::vec!["id", "release", "payload"]);
27066 } else {
27067 panic!("expected CreateTable");
27068 }
27069 }
27070
27071 #[test]
27072 fn release_usable_as_column_ref_in_select_projection() {
27073 // The sentori `0003_partition_events.sql` INSERT-SELECT
27074 // walk references `release` in both column lists; the
27075 // projection-side use exercises `parse_atom`'s relaxed
27076 // identifier set.
27077 parse("SELECT id, release, payload FROM events WHERE id = 1");
27078 }
27079
27080 #[test]
27081 fn release_usable_as_column_ref_in_insert_column_list() {
27082 // INSERT INTO t (id, release, payload) VALUES (…)
27083 parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
27084 }
27085
27086 #[test]
27087 fn alter_column_drop_not_null_uses_keyword_drop_token() {
27088 // Sentori `0013_audit_tombstone.sql` issues
27089 // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
27090 // emits Token::Drop (not Ident("drop")); the parser must
27091 // accept both in the ALTER COLUMN sub-dispatch.
27092 parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
27093 }
27094
27095 #[test]
27096 fn create_index_accepts_parenthesised_expression_key() {
27097 // sentori `0040_events_bundle_idx.sql` shape — JSONB
27098 // expression index. Pre-T4 the parser bailed at the
27099 // inner `(` with "expected column ident or expression,
27100 // got LParen". The Token::LParen arm in CREATE INDEX
27101 // routes through the expression parser instead.
27102 parse(
27103 "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
27104 ON events ((payload->'bundle'->>'id'))",
27105 );
27106 }
27107
27108 // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
27109 // surface as parse errors, never stack overflows (embed hosts
27110 // abort on overflow).
27111 /// The nesting budget is a COUNT; what it has to fit inside is a
27112 /// number of BYTES, and only one of those two is stable across
27113 /// compiler versions. Round 847 measured 30,336 bytes per level
27114 /// after a toolchain move, which puts 64 levels at 1.94 MB and
27115 /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
27116 /// aborted instead of erroring, which is precisely the outcome it
27117 /// exists to rule out.
27118 ///
27119 /// So the budget is metered rather than assumed. The ceiling leaves
27120 /// the depth SPG advertises fitting in a default 2 MiB thread with
27121 /// room to spare, in the debug build, where frames are widest.
27122 #[test]
27123 fn nesting_frame_cost_stays_under_ceiling() {
27124 // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
27125 // thread keeps a margin for whatever called the parser.
27126 const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
27127
27128 frame_meter::reset();
27129 let depth = frame_meter::SAMPLE_HI + 8;
27130 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27131 parse(&sql);
27132
27133 let per_level = frame_meter::bytes_per_level();
27134 {
27135 extern crate std;
27136 std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
27137 }
27138 assert!(
27139 per_level <= CEILING,
27140 "{per_level} bytes per nesting level exceeds {CEILING}; \
27141 {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
27142 in parse_expr_inner / parse_unary rather than lowering the \
27143 depth or widening the stack.",
27144 per_level * MAX_NEST_DEPTH
27145 );
27146 }
27147
27148 #[test]
27149 fn nesting_budget_errors_cleanly() {
27150 let depth = MAX_NEST_DEPTH + 50;
27151 let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
27152 let err = parse_statement(&sql).expect_err("must reject");
27153 assert!(err.message.contains("nests deeper"), "{err:?}");
27154 // Within budget still parses.
27155 let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
27156 parse(&sql);
27157 }
27158
27159 #[test]
27160 fn binary_chain_budget_errors_cleanly() {
27161 let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
27162 let err = parse_statement(&sql).expect_err("must reject");
27163 assert!(err.message.contains("chained binary"), "{err:?}");
27164 // Within budget still parses (chain depth ≤ budget is safe
27165 // for recursive eval/drop on 2 MiB stacks).
27166 let sql = format!("SELECT 1{}", " + 1".repeat(200));
27167 parse(&sql);
27168 }
27169
27170 #[test]
27171 fn in_list_unaffected_by_chain_budget() {
27172 // Flat InList: 20k elements parse fine and stay flat.
27173 let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
27174 let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
27175 let Statement::Select(s) = parse(&sql) else {
27176 panic!("expected select")
27177 };
27178 let Some(Expr::InList { list, negated, .. }) = s.where_ else {
27179 panic!("expected flat InList, got {:?}", s.where_)
27180 };
27181 assert_eq!(list.len(), 20_000);
27182 assert!(!negated);
27183 }
27184
27185 fn lit_int(n: i64) -> Expr {
27186 Expr::Literal(Literal::Integer(n))
27187 }
27188
27189 fn col(name: &str) -> Expr {
27190 Expr::Column(ColumnName {
27191 qualifier: None,
27192 name: name.into(),
27193 })
27194 }
27195
27196 #[test]
27197 fn select_single_integer() {
27198 let s = parse("SELECT 1");
27199 let Statement::Select(s) = s else {
27200 panic!("expected SELECT")
27201 };
27202 assert_eq!(s.items.len(), 1);
27203 assert!(s.from.is_none());
27204 assert!(s.where_.is_none());
27205 }
27206
27207 #[test]
27208 fn select_multiple_literal_kinds() {
27209 let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
27210 let Statement::Select(s) = s else {
27211 panic!("expected SELECT")
27212 };
27213 assert_eq!(s.items.len(), 5);
27214 }
27215
27216 #[test]
27217 fn select_wildcard_from_table() {
27218 let s = parse("SELECT * FROM users");
27219 let Statement::Select(s) = s else {
27220 panic!("expected SELECT")
27221 };
27222 assert!(matches!(s.items[..], [SelectItem::Wildcard]));
27223 assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
27224 }
27225
27226 #[test]
27227 fn select_with_table_alias() {
27228 let s = parse("SELECT * FROM users AS u");
27229 let Statement::Select(s) = s else {
27230 panic!("expected SELECT")
27231 };
27232 let t = &s.from.as_ref().unwrap().primary;
27233 assert_eq!(t.name, "users");
27234 assert_eq!(t.alias.as_deref(), Some("u"));
27235 }
27236
27237 #[test]
27238 fn select_with_where_eq() {
27239 let s = parse("SELECT a FROM t WHERE a = 1");
27240 let Statement::Select(s) = s else {
27241 panic!("expected SELECT")
27242 };
27243 let w = s.where_.unwrap();
27244 assert_eq!(
27245 w,
27246 Expr::Binary {
27247 lhs: Box::new(col("a")),
27248 op: BinOp::Eq,
27249 rhs: Box::new(lit_int(1)),
27250 }
27251 );
27252 }
27253
27254 #[test]
27255 fn arithmetic_precedence() {
27256 let s = parse("SELECT 1 + 2 * 3");
27257 let Statement::Select(s) = s else {
27258 panic!("expected SELECT")
27259 };
27260 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27261 panic!("wildcard?")
27262 };
27263 assert_eq!(
27264 expr,
27265 &Expr::Binary {
27266 lhs: Box::new(lit_int(1)),
27267 op: BinOp::Add,
27268 rhs: Box::new(Expr::Binary {
27269 lhs: Box::new(lit_int(2)),
27270 op: BinOp::Mul,
27271 rhs: Box::new(lit_int(3)),
27272 }),
27273 }
27274 );
27275 }
27276
27277 #[test]
27278 fn parentheses_override_precedence() {
27279 let s = parse("SELECT (1 + 2) * 3");
27280 let Statement::Select(s) = s else {
27281 panic!("expected SELECT")
27282 };
27283 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27284 panic!()
27285 };
27286 assert_eq!(
27287 expr,
27288 &Expr::Binary {
27289 lhs: Box::new(Expr::Binary {
27290 lhs: Box::new(lit_int(1)),
27291 op: BinOp::Add,
27292 rhs: Box::new(lit_int(2)),
27293 }),
27294 op: BinOp::Mul,
27295 rhs: Box::new(lit_int(3)),
27296 }
27297 );
27298 }
27299
27300 #[test]
27301 fn not_binds_below_comparison() {
27302 // `NOT a = 1` should parse as `NOT (a = 1)`.
27303 let s = parse("SELECT NOT a = 1 FROM t");
27304 let Statement::Select(s) = s else {
27305 panic!("expected SELECT")
27306 };
27307 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27308 panic!()
27309 };
27310 assert_eq!(
27311 expr,
27312 &Expr::Unary {
27313 op: UnOp::Not,
27314 expr: Box::new(Expr::Binary {
27315 lhs: Box::new(col("a")),
27316 op: BinOp::Eq,
27317 rhs: Box::new(lit_int(1)),
27318 }),
27319 }
27320 );
27321 }
27322
27323 #[test]
27324 fn unary_minus_binds_above_multiplication() {
27325 // `-a * 2` should be `(-a) * 2`.
27326 let s = parse("SELECT -a * 2 FROM t");
27327 let Statement::Select(s) = s else {
27328 panic!("expected SELECT")
27329 };
27330 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27331 panic!()
27332 };
27333 assert_eq!(
27334 expr,
27335 &Expr::Binary {
27336 lhs: Box::new(Expr::Unary {
27337 op: UnOp::Neg,
27338 expr: Box::new(col("a")),
27339 }),
27340 op: BinOp::Mul,
27341 rhs: Box::new(lit_int(2)),
27342 }
27343 );
27344 }
27345
27346 #[test]
27347 fn qualified_column() {
27348 let s = parse("SELECT t.col FROM t");
27349 let Statement::Select(s) = s else {
27350 panic!("expected SELECT")
27351 };
27352 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27353 panic!()
27354 };
27355 assert_eq!(
27356 expr,
27357 &Expr::Column(ColumnName {
27358 qualifier: Some("t".into()),
27359 name: "col".into()
27360 })
27361 );
27362 }
27363
27364 #[test]
27365 fn select_item_alias_with_as() {
27366 let s = parse("SELECT a AS y FROM t");
27367 let Statement::Select(s) = s else {
27368 panic!("expected SELECT")
27369 };
27370 let SelectItem::Expr { alias, .. } = &s.items[0] else {
27371 panic!()
27372 };
27373 assert_eq!(alias.as_deref(), Some("y"));
27374 }
27375
27376 #[test]
27377 fn trailing_semicolon_accepted() {
27378 let s = parse("SELECT 1;");
27379 let Statement::Select(s) = s else {
27380 panic!("expected SELECT")
27381 };
27382 assert_eq!(s.items.len(), 1);
27383 }
27384
27385 #[test]
27386 fn boolean_chain_with_and_or_not() {
27387 // (NOT a) OR (b AND (NOT c))
27388 let s = parse("SELECT NOT a OR b AND NOT c FROM t");
27389 let Statement::Select(s) = s else {
27390 panic!("expected SELECT")
27391 };
27392 let SelectItem::Expr { expr, .. } = &s.items[0] else {
27393 panic!()
27394 };
27395 let expected = Expr::Binary {
27396 lhs: Box::new(Expr::Unary {
27397 op: UnOp::Not,
27398 expr: Box::new(col("a")),
27399 }),
27400 op: BinOp::Or,
27401 rhs: Box::new(Expr::Binary {
27402 lhs: Box::new(col("b")),
27403 op: BinOp::And,
27404 rhs: Box::new(Expr::Unary {
27405 op: UnOp::Not,
27406 expr: Box::new(col("c")),
27407 }),
27408 }),
27409 };
27410 assert_eq!(expr, &expected);
27411 }
27412
27413 #[test]
27414 fn empty_input_errors() {
27415 // v7.14.0 — pg_dump preambles emit several comment-only
27416 // / blank-line statements that collapse to Statement::
27417 // Empty rather than a parse error. The old "SELECT in
27418 // message" assertion is stale; verify the new contract:
27419 // empty / whitespace / comment-only input parses to
27420 // Statement::Empty.
27421 assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
27422 assert!(matches!(
27423 parse_statement(" \n\t ").unwrap(),
27424 Statement::Empty
27425 ));
27426 // Sanity: malformed-but-non-empty still errors.
27427 assert!(parse_statement("SELECT FROM WHERE").is_err());
27428 }
27429
27430 #[test]
27431 fn unmatched_paren_errors() {
27432 assert!(parse_statement("SELECT (1 + 2").is_err());
27433 }
27434
27435 #[test]
27436 fn display_round_trip_simple_select() {
27437 let original = parse("SELECT a + 1 FROM t WHERE a > 0");
27438 let text = original.to_string();
27439 let again = parse_statement(&text).expect("re-parse");
27440 assert_eq!(original, again);
27441 }
27442
27443 // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
27444
27445 #[test]
27446 fn create_table_single_column() {
27447 let s = parse("CREATE TABLE foo (a INT)");
27448 let Statement::CreateTable(c) = s else {
27449 panic!("expected CreateTable")
27450 };
27451 assert_eq!(c.name, "foo");
27452 assert_eq!(c.columns.len(), 1);
27453 assert_eq!(c.columns[0].name, "a");
27454 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27455 assert!(c.columns[0].nullable);
27456 }
27457
27458 #[test]
27459 fn create_table_multi_column_with_not_null_mix() {
27460 let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
27461 let Statement::CreateTable(c) = s else {
27462 panic!()
27463 };
27464 assert_eq!(c.columns.len(), 4);
27465 assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
27466 assert!(!c.columns[0].nullable);
27467 assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
27468 assert!(c.columns[1].nullable);
27469 assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
27470 assert!(!c.columns[2].nullable);
27471 assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
27472 }
27473
27474 #[test]
27475 fn create_table_bigint_supported() {
27476 let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
27477 let Statement::CreateTable(c) = s else {
27478 panic!()
27479 };
27480 assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
27481 }
27482
27483 #[test]
27484 fn create_table_vector_default_is_f32() {
27485 let s = parse("CREATE TABLE t (v VECTOR(128))");
27486 let Statement::CreateTable(c) = s else {
27487 panic!()
27488 };
27489 assert_eq!(
27490 c.columns[0].ty,
27491 ColumnTypeName::Vector {
27492 dim: 128,
27493 encoding: VecEncoding::F32,
27494 },
27495 );
27496 }
27497
27498 #[test]
27499 fn create_table_vector_using_sq8() {
27500 // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
27501 // Case-insensitive on both `USING` and the encoding name.
27502 for sql in [
27503 "CREATE TABLE t (v VECTOR(128) USING SQ8)",
27504 "CREATE TABLE t (v VECTOR(128) using sq8)",
27505 ] {
27506 let s = parse(sql);
27507 let Statement::CreateTable(c) = s else {
27508 panic!()
27509 };
27510 assert_eq!(
27511 c.columns[0].ty,
27512 ColumnTypeName::Vector {
27513 dim: 128,
27514 encoding: VecEncoding::Sq8,
27515 },
27516 "{sql}",
27517 );
27518 }
27519 }
27520
27521 #[test]
27522 fn create_table_vector_using_unknown_errors() {
27523 // v7.16.1 — the inline `USING <encoding>` shape on
27524 // CREATE TABLE column defs was withdrawn before
27525 // v7.14.0 in favour of `CREATE INDEX … USING hnsw
27526 // (col vector_<metric>_ops)`; the parser now rejects
27527 // USING at column-list position with a clearer
27528 // "expected ',' or ')'" message. Test asserts the
27529 // current rejection, not the old "unknown vector
27530 // encoding" string.
27531 let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
27532 assert!(
27533 err.message.contains("USING")
27534 || err.message.contains("using")
27535 || err.message.contains("')'")
27536 || err.message.contains("','"),
27537 "expected USING/column-list rejection, got: {}",
27538 err.message
27539 );
27540 }
27541
27542 #[test]
27543 fn vector_using_sq8_display_roundtrips() {
27544 // The Display impl must produce text that re-parses to the
27545 // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
27546 let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
27547 let Statement::CreateTable(c) = s else {
27548 panic!()
27549 };
27550 assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
27551 }
27552
27553 #[test]
27554 fn parser_recognises_placeholders() {
27555 use crate::ast::{Expr, SelectItem, Statement};
27556 // $N in expression position parses as Expr::Placeholder(N).
27557 let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
27558 let Statement::Select(sel) = s else { panic!() };
27559 assert!(matches!(
27560 sel.items[0],
27561 SelectItem::Expr {
27562 expr: Expr::Placeholder(1),
27563 alias: None
27564 }
27565 ));
27566 // $2 + 1
27567 let SelectItem::Expr {
27568 expr: Expr::Binary { lhs, rhs, .. },
27569 ..
27570 } = &sel.items[1]
27571 else {
27572 panic!()
27573 };
27574 assert!(matches!(**lhs, Expr::Placeholder(2)));
27575 assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
27576 // WHERE x = $3
27577 let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
27578 panic!()
27579 };
27580 assert!(matches!(**rhs, Expr::Placeholder(3)));
27581 }
27582
27583 #[test]
27584 fn parser_rejects_dollar_zero() {
27585 // $0 is not valid in PG; the lexer rejects it.
27586 assert!(parse_statement("SELECT $0").is_err());
27587 }
27588
27589 #[test]
27590 fn placeholder_display_roundtrips() {
27591 // The Display impl must produce text that re-lexes to the
27592 // same Placeholder token.
27593 let s = parse("SELECT $42 FROM t");
27594 let printed = s.to_string();
27595 assert!(printed.contains("$42"));
27596 let again = parse(&printed);
27597 assert_eq!(s, again);
27598 }
27599
27600 #[test]
27601 fn alter_index_rebuild_bare() {
27602 use crate::ast::{AlterIndexTarget, Statement};
27603 let s = parse("ALTER INDEX my_idx REBUILD");
27604 let Statement::AlterIndex(a) = s else {
27605 panic!("expected AlterIndex, got {s:?}")
27606 };
27607 assert_eq!(a.name, "my_idx");
27608 assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
27609 }
27610
27611 #[test]
27612 fn alter_index_rebuild_with_encoding() {
27613 use crate::ast::{AlterIndexTarget, Statement};
27614 for (sql, want) in [
27615 (
27616 "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
27617 VecEncoding::F32,
27618 ),
27619 (
27620 "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
27621 VecEncoding::Sq8,
27622 ),
27623 (
27624 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27625 VecEncoding::F16,
27626 ),
27627 ] {
27628 let s = parse(sql);
27629 let Statement::AlterIndex(a) = s else {
27630 panic!("{sql}: expected AlterIndex")
27631 };
27632 assert_eq!(a.name, "my_idx");
27633 assert_eq!(
27634 a.target,
27635 AlterIndexTarget::Rebuild {
27636 encoding: Some(want)
27637 },
27638 "{sql}"
27639 );
27640 }
27641 }
27642
27643 #[test]
27644 fn alter_index_rebuild_unknown_encoding_errors() {
27645 let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
27646 assert!(
27647 err.message.contains("unknown vector encoding"),
27648 "got: {}",
27649 err.message
27650 );
27651 }
27652
27653 #[test]
27654 fn alter_index_rebuild_display_roundtrips() {
27655 for (input, want) in [
27656 ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
27657 (
27658 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27659 "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
27660 ),
27661 (
27662 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27663 "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
27664 ),
27665 ] {
27666 let s = parse(input);
27667 assert_eq!(s.to_string(), want);
27668 }
27669 }
27670
27671 #[test]
27672 fn create_table_unknown_type_defers_to_engine() {
27673 // v4.9 picked XML as a parse-time "unsupported column
27674 // type" probe. v7.17.0 Phase 1.4 changed the contract:
27675 // an unknown type ident parses as Text + `user_type_ref`
27676 // so CREATE TABLE can resolve user-defined enum / domain
27677 // types — rejection of truly-unknown types moved to the
27678 // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
27679 // to a first-class built-in, so this probe switched to a
27680 // synthetic name nothing in the lexer will ever recognise.
27681 let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
27682 let Statement::CreateTable(t) = stmt else {
27683 panic!("expected CreateTable");
27684 };
27685 assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
27686 }
27687
27688 #[test]
27689 fn create_table_missing_table_keyword_errors() {
27690 assert!(parse_statement("CREATE x (a INT)").is_err());
27691 }
27692
27693 // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
27694 // `PARTITION OF parent <bounds>` child parse + Display round-trip.
27695
27696 #[test]
27697 fn parse_create_table_partition_by_range() {
27698 use crate::ast::{PartitionBySpec, PartitionKindAst};
27699 let stmt = parse_statement(
27700 "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
27701 payload JSONB) PARTITION BY RANGE (ts)",
27702 )
27703 .unwrap();
27704 let Statement::CreateTable(t) = stmt else {
27705 panic!("expected CreateTable");
27706 };
27707 assert!(t.partition_of.is_none(), "parent has no partition_of");
27708 assert_eq!(t.columns.len(), 3);
27709 let by = t.partition_by.as_ref().expect("expected PARTITION BY");
27710 assert_eq!(
27711 by,
27712 &PartitionBySpec {
27713 kind: PartitionKindAst::Range,
27714 key_columns: alloc::vec!["ts".to_string()],
27715 }
27716 );
27717 // Display round-trip preserves the suffix. `quote_ident`
27718 // only adds double quotes when the ident needs escaping, so
27719 // a plain `ts` survives bare here.
27720 assert!(
27721 t.to_string().contains("PARTITION BY RANGE (ts)"),
27722 "Display lost PARTITION BY suffix: {t}"
27723 );
27724 }
27725
27726 #[test]
27727 fn parse_create_table_partition_of_range() {
27728 use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
27729 let stmt = parse_statement(
27730 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
27731 FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
27732 )
27733 .unwrap();
27734 let Statement::CreateTable(t) = stmt else {
27735 panic!("expected CreateTable");
27736 };
27737 assert!(t.columns.is_empty(), "child inherits columns from parent");
27738 assert!(t.partition_by.is_none());
27739 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27740 assert_eq!(of.parent_name, "events_partitioned");
27741 let PartitionOfSpec { bounds, .. } = of.clone();
27742 match bounds {
27743 PartitionOfBoundsAst::Range { lower, upper } => {
27744 assert!(lower.to_string().contains("2026-06-01"));
27745 assert!(upper.to_string().contains("2026-07-01"));
27746 }
27747 other => panic!("expected Range, got {other:?}"),
27748 }
27749 // Display round-trip emits the FOR VALUES tail. `quote_ident`
27750 // skips quotes when not required, so the parent name appears
27751 // bare here.
27752 let s = t.to_string();
27753 assert!(
27754 s.contains("PARTITION OF events_partitioned"),
27755 "Display lost PARTITION OF: {s}"
27756 );
27757 assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
27758 assert!(s.contains(") TO ("), "Display lost TO: {s}");
27759 }
27760
27761 #[test]
27762 fn parse_create_table_partition_of_default() {
27763 use crate::ast::PartitionOfBoundsAst;
27764 let stmt =
27765 parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
27766 .unwrap();
27767 let Statement::CreateTable(t) = stmt else {
27768 panic!("expected CreateTable");
27769 };
27770 let of = t.partition_of.as_ref().expect("expected PARTITION OF");
27771 assert_eq!(of.parent_name, "events_partitioned");
27772 assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
27773 assert!(
27774 t.to_string()
27775 .contains("PARTITION OF events_partitioned DEFAULT"),
27776 "Display lost DEFAULT: {t}"
27777 );
27778 }
27779
27780 #[test]
27781 fn parse_create_table_partition_by_list() {
27782 // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
27783 // child with `FOR VALUES IN (lit, lit, …)`.
27784 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27785 let parent =
27786 parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
27787 .unwrap();
27788 let Statement::CreateTable(t) = parent else {
27789 panic!("expected CreateTable");
27790 };
27791 let Some(PartitionBySpec {
27792 kind,
27793 ref key_columns,
27794 }) = t.partition_by
27795 else {
27796 panic!("expected PARTITION BY");
27797 };
27798 assert_eq!(kind, PartitionKindAst::List);
27799 assert_eq!(*key_columns, vec!["region".to_string()]);
27800 assert!(t.to_string().contains("PARTITION BY LIST (region)"));
27801
27802 let child = parse_statement(
27803 "CREATE TABLE events_apac PARTITION OF events_listed \
27804 FOR VALUES IN ('jp', 'kr', 'tw')",
27805 )
27806 .unwrap();
27807 let Statement::CreateTable(c) = child else {
27808 panic!("expected CreateTable");
27809 };
27810 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27811 let PartitionOfBoundsAst::List { values } = &of.bounds else {
27812 panic!("expected List bounds, got {:?}", of.bounds);
27813 };
27814 assert_eq!(values.len(), 3);
27815 let disp = c.to_string();
27816 assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
27817 }
27818
27819 #[test]
27820 fn parse_create_table_partition_by_hash() {
27821 // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
27822 // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
27823 use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
27824 let parent =
27825 parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
27826 let Statement::CreateTable(t) = parent else {
27827 panic!("expected CreateTable");
27828 };
27829 let Some(PartitionBySpec {
27830 kind,
27831 ref key_columns,
27832 }) = t.partition_by
27833 else {
27834 panic!("expected PARTITION BY");
27835 };
27836 assert_eq!(kind, PartitionKindAst::Hash);
27837 assert_eq!(*key_columns, vec!["id".to_string()]);
27838 assert!(t.to_string().contains("PARTITION BY HASH (id)"));
27839
27840 let child = parse_statement(
27841 "CREATE TABLE orders_h_0 PARTITION OF orders_h \
27842 FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
27843 )
27844 .unwrap();
27845 let Statement::CreateTable(c) = child else {
27846 panic!("expected CreateTable");
27847 };
27848 let of = c.partition_of.as_ref().expect("expected PARTITION OF");
27849 let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
27850 panic!("expected Hash bounds");
27851 };
27852 assert_eq!(modulus, 4);
27853 assert_eq!(remainder, 0);
27854 let disp = c.to_string();
27855 assert!(
27856 disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
27857 "Display lost HASH bounds: {disp}"
27858 );
27859
27860 // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
27861 let bad = parse_statement(
27862 "CREATE TABLE orders_h_bad PARTITION OF orders_h \
27863 FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
27864 );
27865 let msg = format!("{}", bad.unwrap_err());
27866 assert!(
27867 msg.contains("REMAINDER") && msg.contains("MODULUS"),
27868 "expected REMAINDER/MODULUS validation error: {msg}"
27869 );
27870 }
27871
27872 #[test]
27873 fn parse_create_table_partition_of_rejects_columns() {
27874 // v7.37.6-B contract: PARTITION OF children inherit columns
27875 // from the parent; an explicit list MUST surface as a parse
27876 // error rather than getting silently ignored.
27877 let err = parse_statement(
27878 "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
27879 FOR VALUES FROM ('a') TO ('b')",
27880 );
27881 assert!(err.is_err(), "expected parse error for explicit columns");
27882 let msg = format!("{}", err.unwrap_err());
27883 assert!(
27884 msg.contains("PARTITION OF") && msg.contains("column"),
27885 "error should mention PARTITION OF + columns: {msg}"
27886 );
27887 }
27888
27889 #[test]
27890 fn insert_single_value() {
27891 let s = parse("INSERT INTO foo VALUES (42)");
27892 let Statement::Insert(i) = s else {
27893 panic!("expected Insert")
27894 };
27895 assert_eq!(i.table, "foo");
27896 assert_eq!(i.rows.len(), 1);
27897 assert_eq!(i.rows[0].len(), 1);
27898 assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
27899 }
27900
27901 #[test]
27902 fn insert_multi_value_with_mixed_literals() {
27903 let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
27904 let Statement::Insert(i) = s else { panic!() };
27905 assert_eq!(i.rows.len(), 1);
27906 assert_eq!(i.rows[0].len(), 5);
27907 }
27908
27909 #[test]
27910 fn insert_missing_into_errors() {
27911 assert!(parse_statement("INSERT foo VALUES (1)").is_err());
27912 }
27913
27914 #[test]
27915 fn create_table_round_trip() {
27916 let original =
27917 parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
27918 let text = original.to_string();
27919 let again = parse_statement(&text).expect("re-parse");
27920 assert_eq!(original, again);
27921 }
27922
27923 #[test]
27924 fn insert_round_trip_with_negation_and_string() {
27925 let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
27926 let text = original.to_string();
27927 let again = parse_statement(&text).expect("re-parse");
27928 assert_eq!(original, again);
27929 }
27930
27931 #[test]
27932 fn unknown_keyword_at_statement_start_errors() {
27933 // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
27934 // the top-level dispatch still has no branch to take.
27935 let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
27936 assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
27937 }
27938
27939 // --- v0.8 CREATE INDEX --------------------------------------------------
27940
27941 #[test]
27942 fn create_index_basic() {
27943 let s = parse("CREATE INDEX idx_id ON users (id)");
27944 let Statement::CreateIndex(c) = s else {
27945 panic!("expected CreateIndex")
27946 };
27947 assert_eq!(c.name, "idx_id");
27948 assert_eq!(c.table, "users");
27949 assert_eq!(c.column, "id");
27950 }
27951
27952 #[test]
27953 fn create_index_missing_on_errors() {
27954 assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
27955 }
27956
27957 #[test]
27958 fn create_index_missing_paren_errors() {
27959 assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
27960 }
27961
27962 #[test]
27963 fn create_index_round_trip() {
27964 let original = parse("CREATE INDEX by_name ON users (name)");
27965 let again = parse_statement(&original.to_string()).unwrap();
27966 assert_eq!(original, again);
27967 }
27968
27969 // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
27970
27971 #[test]
27972 fn create_unique_index_basic() {
27973 let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
27974 let Statement::CreateIndex(c) = s else {
27975 panic!("expected CreateIndex");
27976 };
27977 assert!(c.is_unique);
27978 assert_eq!(c.column, "a");
27979 assert!(c.partial_predicate.is_none());
27980 }
27981
27982 #[test]
27983 fn create_unique_index_partial() {
27984 // mailrs's email_templates "one default per user" shape.
27985 let s = parse(
27986 "CREATE UNIQUE INDEX idx_email_templates_user_default \
27987 ON email_templates (user_address) WHERE is_default = true",
27988 );
27989 let Statement::CreateIndex(c) = s else {
27990 panic!("expected CreateIndex");
27991 };
27992 assert!(c.is_unique);
27993 assert_eq!(c.table, "email_templates");
27994 assert_eq!(c.column, "user_address");
27995 assert!(c.partial_predicate.is_some());
27996 }
27997
27998 #[test]
27999 fn create_unique_index_composite_with_predicate() {
28000 // mailrs's calendar_events instance: composite columns.
28001 let s = parse(
28002 "CREATE UNIQUE INDEX uq_calendar_events_instance \
28003 ON calendar_events (calendar_id, uid, recurrence_id) \
28004 WHERE recurrence_id IS NOT NULL",
28005 );
28006 let Statement::CreateIndex(c) = s else {
28007 panic!("expected CreateIndex");
28008 };
28009 assert!(c.is_unique);
28010 assert_eq!(c.column, "calendar_id");
28011 assert_eq!(
28012 c.extra_columns,
28013 vec!["uid".to_string(), "recurrence_id".to_string()]
28014 );
28015 assert!(c.partial_predicate.is_some());
28016 }
28017
28018 #[test]
28019 fn create_unique_index_using_btree_ok() {
28020 let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28021 assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28022 }
28023
28024 #[test]
28025 fn create_unique_index_using_hnsw_rejected() {
28026 let err =
28027 parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28028 assert!(err.message.contains("UNIQUE"), "{}", err.message);
28029 }
28030
28031 #[test]
28032 fn create_unique_index_round_trip() {
28033 let original = parse(
28034 "CREATE UNIQUE INDEX uq_calendar_events_master \
28035 ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28036 );
28037 let again = parse_statement(&original.to_string()).unwrap();
28038 assert_eq!(original, again);
28039 }
28040
28041 #[test]
28042 fn create_unique_without_index_errors() {
28043 let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28044 // v7.39 (round 340, V56) — PG 18.4, verbatim.
28045 assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28046 }
28047
28048 // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28049
28050 #[test]
28051 fn create_table_bytea_column() {
28052 let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28053 let Statement::CreateTable(c) = s else {
28054 panic!("expected CreateTable");
28055 };
28056 assert_eq!(c.columns.len(), 2);
28057 assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28058 assert!(!c.columns[1].nullable);
28059 }
28060
28061 #[test]
28062 fn create_table_bytes_alias_column() {
28063 let s = parse("CREATE TABLE t (blob BYTES)");
28064 let Statement::CreateTable(c) = s else {
28065 panic!("expected CreateTable");
28066 };
28067 assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
28068 }
28069
28070 #[test]
28071 fn bytea_round_trip_display() {
28072 let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
28073 let again = parse_statement(&original.to_string()).unwrap();
28074 assert_eq!(original, again);
28075 }
28076
28077 // --- v0.9 transactions -------------------------------------------------
28078
28079 #[test]
28080 fn begin_commit_rollback_parse_as_unit_variants() {
28081 assert_eq!(parse("BEGIN"), Statement::Begin(None));
28082 assert_eq!(parse("COMMIT"), Statement::Commit);
28083 assert_eq!(parse("ROLLBACK"), Statement::Rollback);
28084 // Trailing semicolons accepted too.
28085 assert_eq!(parse("BEGIN;"), Statement::Begin(None));
28086 // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
28087 // statement (with or without the WORK/TRANSACTION noise word).
28088 assert_eq!(
28089 parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
28090 Statement::Begin(Some(IsolationLevel::RepeatableRead))
28091 );
28092 assert_eq!(
28093 parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
28094 Statement::Begin(Some(IsolationLevel::Serializable))
28095 );
28096 // A non-isolation mode keeps the session default (None).
28097 assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
28098 }
28099
28100 // --- v1.2: pgvector distance ops + ::vector cast --------------------
28101
28102 #[test]
28103 fn inner_product_binop_parses() {
28104 let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
28105 let Statement::Select(s) = s else { panic!() };
28106 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28107 panic!()
28108 };
28109 assert!(matches!(
28110 expr,
28111 Expr::Binary {
28112 op: BinOp::InnerProduct,
28113 ..
28114 }
28115 ));
28116 }
28117
28118 #[test]
28119 fn cosine_distance_binop_parses() {
28120 let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
28121 let Statement::Select(s) = s else { panic!() };
28122 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28123 panic!()
28124 };
28125 assert!(matches!(
28126 expr,
28127 Expr::Binary {
28128 op: BinOp::CosineDistance,
28129 ..
28130 }
28131 ));
28132 }
28133
28134 #[test]
28135 fn vector_cast_postfix_wraps_string_literal() {
28136 let s = parse("SELECT '[1,2,3]'::vector FROM t");
28137 let Statement::Select(s) = s else { panic!() };
28138 let SelectItem::Expr { expr, .. } = &s.items[0] else {
28139 panic!()
28140 };
28141 assert!(matches!(
28142 expr,
28143 Expr::Cast {
28144 target: CastTarget::Vector,
28145 ..
28146 }
28147 ));
28148 }
28149
28150 #[test]
28151 fn unsupported_cast_target_errors() {
28152 // v7.37.5 ship triage promoted the parser to accept every
28153 // ident as a `CastTarget::Named(canonical)`; the engine
28154 // surfaces the "unsupported cast target" error at eval
28155 // time when `type_name_to_data_type` can't resolve it.
28156 // Parser-side error now requires a NON-ident after `::`
28157 // (e.g. a punctuation token).
28158 let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
28159 assert_eq!(err.message, "syntax error at or near \",\"");
28160 }
28161
28162 #[test]
28163 fn tx_statements_round_trip() {
28164 for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
28165 let original = parse(q);
28166 let again = parse_statement(&original.to_string()).unwrap();
28167 assert_eq!(original, again);
28168 }
28169 }
28170
28171 #[test]
28172 fn interval_text_parsing_units() {
28173 // v7.37.5 β — three-field shape `(months, days, micros)` so
28174 // `'1 day'` and `'24 hours'` no longer collide (PG parity).
28175 // Single unit.
28176 assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
28177 assert_eq!(
28178 parse_interval_text("24 hours"),
28179 Some((0, 0, 86_400_000_000))
28180 );
28181 assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
28182 assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
28183 assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
28184 assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
28185 // Compound spans accumulate per-dimension.
28186 assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
28187 assert_eq!(
28188 parse_interval_text("1 day 2 hours"),
28189 Some((0, 1, 7_200_000_000))
28190 );
28191 // Negative numbers carry through per-dimension.
28192 assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
28193 // Bad shapes return None.
28194 assert_eq!(parse_interval_text(""), None);
28195 assert_eq!(parse_interval_text("garbage"), None);
28196 assert_eq!(parse_interval_text("1 fortnight"), None);
28197 // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
28198 // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
28199 assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
28200 assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
28201 assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
28202 }
28203
28204 #[test]
28205 fn interval_literal_roundtrips_via_display() {
28206 let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
28207 let s = parsed.to_string();
28208 // Display preserves the original text verbatim.
28209 assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
28210 // And re-parsing yields a structurally equal statement.
28211 let again = parse_statement(&s).unwrap();
28212 assert_eq!(parsed, again);
28213 }
28214
28215 // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
28216
28217 #[test]
28218 fn parser_recognises_create_publication_bare() {
28219 let s = parse("CREATE PUBLICATION pub_a");
28220 let Statement::CreatePublication(p) = s else {
28221 panic!("expected CreatePublication, got {s:?}")
28222 };
28223 assert_eq!(p.name, "pub_a");
28224 assert_eq!(p.scope, PublicationScope::AllTables);
28225 }
28226
28227 #[test]
28228 fn parser_recognises_create_publication_for_all_tables() {
28229 let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
28230 let Statement::CreatePublication(p) = s else {
28231 panic!("expected CreatePublication, got {s:?}")
28232 };
28233 assert_eq!(p.name, "pub_a");
28234 assert_eq!(p.scope, PublicationScope::AllTables);
28235 }
28236
28237 #[test]
28238 fn parser_recognises_drop_publication() {
28239 let s = parse("DROP PUBLICATION pub_a");
28240 let Statement::DropPublication { name, .. } = s else {
28241 panic!("expected DropPublication, got {s:?}")
28242 };
28243 assert_eq!(name, "pub_a");
28244 }
28245
28246 #[test]
28247 fn parser_recognises_for_table_list() {
28248 let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
28249 let Statement::CreatePublication(p) = s else {
28250 panic!("expected CreatePublication, got {s:?}")
28251 };
28252 assert_eq!(p.name, "pub_a");
28253 let PublicationScope::ForTables(ts) = p.scope else {
28254 panic!("expected ForTables scope")
28255 };
28256 assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
28257 }
28258
28259 #[test]
28260 fn parser_rejects_bare_for_tables_and_takes_in_schema() {
28261 // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
28262 // is rejected (`invalid publication object list`; the old
28263 // test pinned an unverifiable "PG 19 accepts both" claim);
28264 // TABLES pairs with IN SCHEMA.
28265 let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
28266 .expect_err("bare FOR TABLES must reject");
28267 assert!(
28268 alloc::format!("{err}").contains("invalid publication object list"),
28269 "got: {err}"
28270 );
28271 let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
28272 let Statement::CreatePublication(p) = s else {
28273 panic!("expected CreatePublication, got {s:?}")
28274 };
28275 let PublicationScope::TablesInSchema(schema) = p.scope else {
28276 panic!("expected TablesInSchema")
28277 };
28278 assert_eq!(schema, "public");
28279 }
28280
28281 #[test]
28282 fn parser_recognises_for_all_tables_except_list() {
28283 let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
28284 let Statement::CreatePublication(p) = s else {
28285 panic!()
28286 };
28287 let PublicationScope::AllTablesExcept(ts) = p.scope else {
28288 panic!("expected AllTablesExcept")
28289 };
28290 assert_eq!(ts, alloc::vec!["t1", "t2"]);
28291 }
28292
28293 #[test]
28294 fn parser_rejects_for_table_with_empty_list() {
28295 // `FOR TABLE` with nothing after is a parse error.
28296 let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
28297 .expect_err("must error on empty list");
28298 // No specific message asserted — the call falls through to
28299 // expect_ident_like which yields "expected identifier, got …".
28300 assert!(!err.message.is_empty());
28301 }
28302
28303 #[test]
28304 fn parser_recognises_show_publications() {
28305 // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
28306 // bare ident in this position, NOT a reserved keyword.
28307 let s = parse("SHOW PUBLICATIONS");
28308 assert!(matches!(s, Statement::ShowPublications));
28309 }
28310
28311 // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
28312
28313 #[test]
28314 fn parser_recognises_create_subscription_single_publication() {
28315 let s = parse(
28316 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
28317 );
28318 let Statement::CreateSubscription(c) = s else {
28319 panic!("expected CreateSubscription, got {s:?}")
28320 };
28321 assert_eq!(c.name, "sub_a");
28322 assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
28323 assert_eq!(c.publications, alloc::vec!["pub_a"]);
28324 }
28325
28326 #[test]
28327 fn parser_recognises_create_subscription_multi_publication() {
28328 let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
28329 let Statement::CreateSubscription(c) = s else {
28330 panic!()
28331 };
28332 assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
28333 }
28334
28335 #[test]
28336 fn parser_rejects_create_subscription_missing_connection() {
28337 let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
28338 .expect_err("must error on missing CONNECTION");
28339 assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
28340 }
28341
28342 #[test]
28343 fn parser_rejects_create_subscription_missing_publication() {
28344 let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
28345 .expect_err("must error on missing PUBLICATION");
28346 assert_eq!(err.message, "syntax error at end of input");
28347 }
28348
28349 #[test]
28350 fn parser_recognises_drop_subscription() {
28351 let s = parse("DROP SUBSCRIPTION sub_a");
28352 let Statement::DropSubscription { name, .. } = s else {
28353 panic!("expected DropSubscription, got {s:?}")
28354 };
28355 assert_eq!(name, "sub_a");
28356 }
28357
28358 #[test]
28359 fn parser_recognises_show_subscriptions() {
28360 let s = parse("SHOW SUBSCRIPTIONS");
28361 assert!(matches!(s, Statement::ShowSubscriptions));
28362 }
28363
28364 #[test]
28365 fn parser_recognises_wait_for_wal_position_no_timeout() {
28366 let s = parse("WAIT FOR WAL POSITION 12345");
28367 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28368 panic!("expected WaitForWalPosition, got {s:?}")
28369 };
28370 assert_eq!(pos, 12345);
28371 assert!(timeout_ms.is_none());
28372 }
28373
28374 #[test]
28375 fn parser_recognises_wait_for_wal_position_with_timeout() {
28376 let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
28377 let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
28378 panic!()
28379 };
28380 assert_eq!(pos, 67890);
28381 assert_eq!(timeout_ms, Some(5000));
28382 }
28383
28384 #[test]
28385 fn parser_rejects_wait_with_negative_position() {
28386 // The lexer treats `-` as a token; `expect_u64_literal`
28387 // only sees the Integer that follows, so the negative
28388 // arrives as a unary-minus expression at higher levels.
28389 // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
28390 // parse error one way or another.
28391 let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
28392 assert!(!err.message.is_empty());
28393 }
28394
28395 #[test]
28396 fn parser_recognises_bare_analyze() {
28397 let s = parse("ANALYZE");
28398 assert!(matches!(s, Statement::Analyze(None)));
28399 }
28400
28401 #[test]
28402 fn parser_recognises_analyze_with_table() {
28403 let s = parse("ANALYZE users");
28404 let Statement::Analyze(Some(name)) = s else {
28405 panic!("expected Analyze, got {s:?}")
28406 };
28407 assert_eq!(name, "users");
28408 }
28409
28410 #[test]
28411 fn parser_recognises_analyze_with_quoted_table() {
28412 let s = parse("ANALYZE \"Mixed Case\"");
28413 let Statement::Analyze(Some(name)) = s else {
28414 panic!()
28415 };
28416 assert_eq!(name, "Mixed Case");
28417 }
28418
28419 #[test]
28420 fn parser_rejects_analyze_with_garbage_token() {
28421 let err = parse_statement("ANALYZE 42").expect_err("must error");
28422 assert!(!err.message.is_empty());
28423 }
28424
28425 #[test]
28426 fn analyze_display_roundtrips() {
28427 for sql in ["ANALYZE", "ANALYZE users"] {
28428 let s = parse(sql);
28429 let printed = s.to_string();
28430 let again = parse_statement(&printed)
28431 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28432 assert_eq!(s, again);
28433 }
28434 }
28435
28436 #[test]
28437 fn wait_for_display_roundtrips() {
28438 for sql in [
28439 "WAIT FOR WAL POSITION 12345",
28440 "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
28441 ] {
28442 let s = parse(sql);
28443 let printed = s.to_string();
28444 let again = parse_statement(&printed)
28445 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28446 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28447 }
28448 }
28449
28450 #[test]
28451 fn subscription_ddl_display_roundtrips() {
28452 for sql in [
28453 "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
28454 "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
28455 "DROP SUBSCRIPTION sub_a",
28456 "SHOW SUBSCRIPTIONS",
28457 ] {
28458 let s = parse(sql);
28459 let printed = s.to_string();
28460 let again = parse_statement(&printed)
28461 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28462 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28463 }
28464 }
28465
28466 #[test]
28467 fn parser_drop_dispatches_user_vs_publication() {
28468 // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
28469 // tokenises DROP. Both targets must still parse.
28470 let s = parse("DROP USER 'alice'");
28471 let Statement::DropUser { name, .. } = s else {
28472 panic!("expected DropUser, got {s:?}")
28473 };
28474 assert_eq!(name, "alice");
28475 // And DROP PUBLICATION lands the new variant.
28476 let s = parse("DROP PUBLICATION p1");
28477 assert!(matches!(s, Statement::DropPublication { .. }));
28478 }
28479
28480 #[test]
28481 fn publication_ddl_display_roundtrips() {
28482 // Every CREATE PUBLICATION variant must Display → parse →
28483 // same AST. v6.1.3 covers all three scope shapes.
28484 for sql in [
28485 "CREATE PUBLICATION pub_a",
28486 "CREATE PUBLICATION pub_a FOR ALL TABLES",
28487 "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
28488 "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
28489 "DROP PUBLICATION pub_a",
28490 "SHOW PUBLICATIONS",
28491 ] {
28492 let s = parse(sql);
28493 let printed = s.to_string();
28494 let again = parse_statement(&printed)
28495 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28496 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28497 }
28498 }
28499
28500 // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
28501
28502 #[test]
28503 fn create_function_returns_trigger_plpgsql_minimal() {
28504 let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
28505 let s = parse(sql);
28506 let Statement::CreateFunction(f) = s else {
28507 panic!("expected CreateFunction");
28508 };
28509 assert_eq!(f.name, "noop");
28510 assert!(!f.or_replace);
28511 assert!(f.args.is_empty());
28512 assert!(matches!(f.returns, FunctionReturn::Trigger));
28513 assert_eq!(f.language, "plpgsql");
28514 let FunctionBody::PlPgSql(block) = f.body else {
28515 panic!("expected PlPgSql body");
28516 };
28517 assert_eq!(block.statements.len(), 1);
28518 assert!(matches!(
28519 block.statements[0],
28520 PlPgSqlStmt::Return(ReturnTarget::New)
28521 ));
28522 }
28523
28524 #[test]
28525 fn create_function_or_replace_with_assignment() {
28526 // mailrs-shape trigger function: NEW.col := to_tsvector(...);
28527 // RETURN NEW.
28528 let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
28529BEGIN
28530 NEW.search_vector := to_tsvector('english', NEW.subject);
28531 RETURN NEW;
28532END;
28533$$";
28534 let s = parse(sql);
28535 let Statement::CreateFunction(f) = s else {
28536 panic!("expected CreateFunction");
28537 };
28538 assert!(f.or_replace);
28539 let FunctionBody::PlPgSql(block) = &f.body else {
28540 panic!("expected PlPgSql body");
28541 };
28542 assert_eq!(block.statements.len(), 2);
28543 // First statement: NEW.search_vector := to_tsvector(...)
28544 let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
28545 panic!("expected Assign as first stmt");
28546 };
28547 match target {
28548 AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
28549 other => panic!("expected NEW.col, got {other:?}"),
28550 }
28551 // Second statement: RETURN NEW
28552 assert!(matches!(
28553 block.statements[1],
28554 PlPgSqlStmt::Return(ReturnTarget::New)
28555 ));
28556 }
28557
28558 #[test]
28559 fn create_trigger_after_insert_or_update() {
28560 let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
28561 let s = parse(sql);
28562 let Statement::CreateTrigger(t) = s else {
28563 panic!("expected CreateTrigger");
28564 };
28565 assert_eq!(t.name, "tg");
28566 assert_eq!(t.table, "messages");
28567 assert_eq!(t.timing, TriggerTiming::After);
28568 assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
28569 assert_eq!(t.for_each, TriggerForEach::Row);
28570 assert_eq!(t.function, "update_sv");
28571 }
28572
28573 #[test]
28574 fn create_trigger_before_delete_execute_procedure_alias() {
28575 // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
28576 let sql =
28577 "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
28578 let s = parse(sql);
28579 let Statement::CreateTrigger(t) = s else {
28580 panic!("expected CreateTrigger");
28581 };
28582 assert_eq!(t.timing, TriggerTiming::Before);
28583 assert_eq!(t.events, vec![TriggerEvent::Delete]);
28584 }
28585
28586 #[test]
28587 fn drop_trigger_if_exists_round_trips() {
28588 // No parser support for DROP TRIGGER yet — added in v7.12.5
28589 // alongside the broader DROP …{IF EXISTS} cleanup. The
28590 // AST + Display impls are in place so we round-trip via
28591 // construction:
28592 let s = Statement::DropTrigger {
28593 name: "tg".into(),
28594 table: "messages".into(),
28595 if_exists: true,
28596 };
28597 assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
28598 }
28599
28600 #[test]
28601 fn trigger_ddl_display_roundtrips_through_parser() {
28602 // CREATE TRIGGER + its referenced CREATE FUNCTION must
28603 // Display → parse → same AST (modulo PL/pgSQL body
28604 // formatting which is parser-canonicalised).
28605 for sql in [
28606 "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
28607 "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
28608 ] {
28609 let s = parse(sql);
28610 let printed = s.to_string();
28611 let again = parse_statement(&printed)
28612 .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
28613 assert_eq!(s, again, "round-trip mismatch for {sql:?}");
28614 }
28615 }
28616}