Skip to main content

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            // PG psql backslash meta-commands that newer
73            // pg_dump versions emit unescaped (\restrict /
74            // \unrestrict). Real psql intercepts these; SPG's
75            // PG-wire sees them as raw text.
76            | "\\restrict"
77            | "\\unrestrict"
78            // v7.17.0 Phase 4.1 — MySQL `DELIMITER //` and
79            // `DELIMITER ;` directives. Technically client-side
80            // (the `mysql` CLI uses them to set the statement
81            // terminator), not SQL — but mysqldump and stored-
82            // procedure scripts emit them inline. SPG's parser
83            // sees one statement at a time and doesn't care
84            // about the terminator, so consume DELIMITER lines
85            // as Empty.
86            | "delimiter"
87            // v7.37.17 (17.6 siblings) — additional PG maintenance /
88            // session-state statements pg_dump + application startup
89            // scripts emit. SPG has no matching session-state to
90            // discard (no prepared-plan cache surface, no temp
91            // sequences), no matching security-label / storage-
92            // option to apply, no separate CREATE/DROP CAST that
93            // affects execution.
94            // v7.37.17 (17.6 siblings) — PG role-cleanup statements
95            // pg_dump / pg_dumpall emit around DROP ROLE:
96            //   REASSIGN OWNED BY <role> [, ...] TO <newrole>
97            //   DROP OWNED BY <role> [, ...] [CASCADE | RESTRICT]
98            // Both operate on the role's owned objects; SPG has no
99            // role-owner model, so accept-and-no-op.
100            // v7.37.17 (17.6 sibling) — LOAD '<library>'. pg_dump
101            // + extension scripts use LOAD to preload shared
102            // libraries. SPG doesn't have a shared-library extension
103            // point today (extensions ship as first-class crates
104            // linked at build time); accept as a no-op.
105            | "load"
106    )
107}
108
109/// v7.37.43-T4 — PG-unreserved keywords that are legal identifiers
110/// per `pg_get_keywords()`. SPG tokenizes these as named variants
111/// so the parser can dispatch on them in their owning contexts
112/// (`RELEASE SAVEPOINT`, `SHOW name`, `BEGIN`/`COMMIT`/`ROLLBACK`,
113/// `CREATE INDEX`, etc.), but they MUST stay usable as table /
114/// column / alias names — that's the PG contract for unreserved
115/// keywords (see PG docs Appendix C.1).
116///
117/// Before this generalisation, sentori migration 0001_init.sql
118/// `release TEXT NOT NULL` blew up the parser with "expected
119/// identifier, got Release", and the same gap stalked every
120/// SPG drop-in user whose schema had a column / alias named
121/// `release` / `index` / `tables` / `show` / `savepoint` /
122/// `begin` / `commit` / `rollback` / `drop` / `insert` / `values`
123/// / `limit` / `partition`. PG accepts all of them as identifiers
124/// when unquoted, so SPG must too.
125///
126/// Returns the canonical lowercase identifier text when the token
127/// belongs to PG's unreserved class, `None` otherwise. Used by
128/// `expect_ident_like` (column / table / alias names) so the
129/// generalisation applies everywhere an identifier may appear,
130/// not just in the contexts these tokens were introduced for.
131fn unreserved_keyword_text(tok: &Token) -> Option<String> {
132    let s = match tok {
133        // PG keyword class: unreserved or col_name.
134        //
135        Token::Release => "release",
136        Token::Savepoint => "savepoint",
137        Token::Show => "show",
138        Token::Index => "index",
139        Token::Begin => "begin",
140        Token::Commit => "commit",
141        Token::Rollback => "rollback",
142        Token::Drop => "drop",
143        Token::Insert => "insert",
144        Token::Values => "values",
145        Token::Limit => "limit",
146        Token::Partition => "partition",
147        Token::Tables => "tables",
148        Token::Connection => "connection",
149        Token::Publication => "publication",
150        Token::Subscription => "subscription",
151        Token::Interval => "interval",
152        // `extract` is non-reserved in PG too (it's a function the
153        // parser dispatches via context — outside that context it's
154        // a plain identifier).
155        Token::Extract => "extract",
156        Token::Offset => "offset",
157        // `to` is reserved in PG (used in many "AS … TO …" forms), so
158        // it is NOT relaxed here. Same for `from`, `where`, `as`,
159        // `select`, `not`, `and`, `or`, `null`, `true`, `false`,
160        // `create`, `table`, `into`, `on`, `order`, `by`, `having`,
161        // `group`, `distinct`, `union`, `all`, `join`, `inner`,
162        // `left`, `cross`, `outer`, `default`, `is`, `between`,
163        // `in`, `like`, `for`, `except`, `desc`, `asc`, `partition`
164        // (partial — keep partition as unreserved per modern PG).
165        _ => return None,
166    };
167    Some(s.to_string())
168}
169
170/// v7.9.22 — recognise pgvector / SPG vector-index opclass names
171/// in CREATE INDEX. SPG's HNSW already routes by query operator;
172/// the opclass is accepted for `pg_dump` compatibility (mailrs
173/// migration follow-up G5).
174/// v7.13.0 — extended to recognise PG built-in / pg_trgm opclasses
175/// (mailrs round-5 G5). These are tokens-only acceptance — SPG
176/// doesn't change index behaviour based on them.
177/// v7.37.17 (17.6 siblings) — the four PG `each` SRFs share one
178/// FROM-clause pipeline; the stored name tells the executor whether
179/// the value column keeps JSON rendering (`jsonb_each` / `json_each`)
180/// or unwraps to text (`*_each_text`).
181fn is_json_each_name(s: &str) -> bool {
182    s.eq_ignore_ascii_case("jsonb_each_text")
183        || s.eq_ignore_ascii_case("jsonb_each")
184        || s.eq_ignore_ascii_case("json_each_text")
185        || s.eq_ignore_ascii_case("json_each")
186}
187
188/// v7.38 (read01, T14) — resolve named function arguments (`argname => value`)
189/// to positional order for the `make_*` family (the AST stays positional).
190/// Positional args fill slots left-to-right; a named arg goes to its registered
191/// slot; unfilled slots default to integer 0 (PG's optional make_interval
192/// fields — the make_date/time arity is still checked at eval time).
193fn reorder_named_args(
194    fname: &str,
195    args: Vec<Expr>,
196    names: &[Option<String>],
197) -> Result<Vec<Expr>, String> {
198    let params: &[&str] = match fname.to_ascii_lowercase().as_str() {
199        "make_date" => &["year", "month", "day"],
200        "make_time" => &["hour", "min", "sec"],
201        "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
202        "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
203        other => {
204            return Err(alloc::format!(
205                "function {other}(...) does not support named arguments"
206            ));
207        }
208    };
209    let mut slots: Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
210    let mut next_positional = 0usize;
211    for (arg, name) in args.into_iter().zip(names.iter()) {
212        let idx = match name {
213            Some(n) => params
214                .iter()
215                .position(|p| p.eq_ignore_ascii_case(n))
216                .ok_or_else(|| alloc::format!("{fname}(...) has no argument named \"{n}\""))?,
217            None => {
218                let i = next_positional;
219                next_positional += 1;
220                i
221            }
222        };
223        if idx >= slots.len() {
224            return Err(alloc::format!("too many arguments for {fname}(...)"));
225        }
226        if slots[idx].is_some() {
227            return Err(alloc::format!(
228                "argument \"{}\" specified more than once",
229                params[idx]
230            ));
231        }
232        slots[idx] = Some(arg);
233    }
234    Ok(slots
235        .into_iter()
236        .map(|s| s.unwrap_or(Expr::Literal(Literal::Integer(0))))
237        .collect())
238}
239
240/// v7.38 (read01) — parse a lexer `Token::Numeric` source string (digits with
241/// an optional single `.`, no sign, no exponent) into `(unscaled, scale)` for
242/// `Literal::Numeric`. Returns `None` if the mantissa overflows i128.
243/// v7.39 (read01 numeric.c) — the result of expanding an `1.5e3`-style
244/// scientific literal into PG's plain NUMERIC decimal form.
245#[derive(Debug)]
246pub enum SciExpanded {
247    /// Plain decimal string ("1.5e3" → "1500", "1e-5" → "0.00001").
248    Expanded(String),
249    /// Exponent pushes the value outside PG's numeric format
250    /// (more than 131072 integer digits or 16383 fractional digits).
251    Overflow,
252    /// Not a `[±]digits[.digits]e[±]digits` literal at all.
253    NotScientific,
254}
255
256/// Expand scientific notation into a plain decimal string by moving the
257/// decimal point — no float round-trip, so the value stays exact. PG treats
258/// such literals as NUMERIC; the digit-count caps mirror PG's numeric format
259/// limits ("value overflows numeric format").
260pub fn expand_scientific_literal(s: &str) -> SciExpanded {
261    let s = s.trim();
262    let Some(epos) = s.find(['e', 'E']) else {
263        return SciExpanded::NotScientific;
264    };
265    let (mant, exp_str) = (&s[..epos], &s[epos + 1..]);
266    let Ok(exp) = exp_str.parse::<i64>() else {
267        return SciExpanded::NotScientific;
268    };
269    let (neg, mant) = match mant.strip_prefix('-') {
270        Some(r) => (true, r),
271        None => (false, mant.strip_prefix('+').unwrap_or(mant)),
272    };
273    let (int_part, frac_part) = match mant.split_once('.') {
274        Some((i, f)) => (i, f),
275        None => (mant, ""),
276    };
277    if (int_part.is_empty() && frac_part.is_empty())
278        || !int_part.bytes().all(|b| b.is_ascii_digit())
279        || !frac_part.bytes().all(|b| b.is_ascii_digit())
280    {
281        return SciExpanded::NotScientific;
282    }
283    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
284    digits.push_str(int_part);
285    digits.push_str(frac_part);
286    // Decimal point position within `digits` after applying the exponent.
287    let new_point = int_part.len() as i64 + exp;
288    // PG's numeric format: up to 131072 digits before the point, 16383 after.
289    if new_point > 131_072 {
290        return SciExpanded::Overflow;
291    }
292    if (digits.len() as i64 - new_point) > 16_383 {
293        return SciExpanded::Overflow;
294    }
295    let sign = if neg { "-" } else { "" };
296    let plain = if new_point <= 0 {
297        let mut out = String::with_capacity(digits.len() + 2 + (-new_point) as usize);
298        out.push_str("0.");
299        for _ in 0..(-new_point) {
300            out.push('0');
301        }
302        out.push_str(&digits);
303        out
304    } else if (new_point as usize) >= digits.len() {
305        let mut out = digits;
306        for _ in 0..(new_point as usize - out.len()) {
307            out.push('0');
308        }
309        out
310    } else {
311        let mut out = String::with_capacity(digits.len() + 1);
312        out.push_str(&digits[..new_point as usize]);
313        out.push('.');
314        out.push_str(&digits[new_point as usize..]);
315        out
316    };
317    SciExpanded::Expanded(alloc::format!("{sign}{plain}"))
318}
319
320/// v7.39 (round 367, M20) — lower a MySQL hexadecimal binary-string
321/// literal (`0x…` / `X'…'`) onto the existing bytea cast. The hex digits
322/// are left-padded to an even count (`0x123` → byte string `01 23`, per
323/// MariaDB) and handed to the PG bytea input format (`\x…`).
324#[inline(never)]
325fn hex_literal_to_bytea_expr(hex: &str) -> Expr {
326    let padded = if hex.len() % 2 == 1 {
327        alloc::format!("0{hex}")
328    } else {
329        hex.to_string()
330    };
331    Expr::Cast {
332        expr: alloc::boxed::Box::new(Expr::Literal(Literal::String(alloc::format!(
333            "\\x{padded}"
334        )))),
335        target: CastTarget::Named("bytea".to_string()),
336    }
337}
338
339/// v7.39 (round 367, M20) — lower a MySQL bit-value literal (`b'1010'`)
340/// onto the bytea cast. The bits are read big-endian and left-padded to a
341/// whole number of bytes (`b'1010'` → one byte `0x0A`, per MariaDB).
342#[inline(never)]
343fn bits_literal_to_bytea_expr(bits: &str) -> Expr {
344    let pad = (8 - bits.len() % 8) % 8;
345    let mut hex = String::with_capacity((bits.len() + pad).div_ceil(4));
346    let padded: String = core::iter::repeat_n('0', pad).chain(bits.chars()).collect();
347    for nibble in padded.as_bytes().chunks(4) {
348        let mut v = 0u8;
349        for &b in nibble {
350            v = (v << 1) | (b - b'0');
351        }
352        hex.push(char::from_digit(u32::from(v), 16).unwrap_or('0'));
353    }
354    hex_literal_to_bytea_expr(&hex)
355}
356
357/// Resolve a lexer `Token::Numeric` into its literal. PG semantics: a dotted
358/// or over-i64 literal is exact NUMERIC; scientific notation is NUMERIC too
359/// (expanded to the plain decimal form); only a fractional depth beyond SPG's
360/// scale width (u8) falls back to double precision.
361///
362/// v7.38.19 — that sentence used to end "(recorded delta)". Measured
363/// against PG 18.4: a literal with 300 fractional digits round-trips
364/// identically on both engines, so whatever the note described is gone.
365/// It is RD-7 in `docs/RECORDED_DELTAS.md`, under "corrected by
366/// measurement" rather than under "open".
367/// Kept out of the parse_expr recursion frame — see the call site.
368#[inline(never)]
369fn numeric_token_to_literal(s: String) -> Result<Literal, String> {
370    match parse_decimal_literal(&s) {
371        Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
372        // v7.38 (read01, T3.C3) — a plain decimal too wide for i128 keeps
373        // its exact value as a NumericBig.
374        None if !s.contains(['e', 'E']) => Ok(Literal::NumericBig(s)),
375        // v7.39 (read01 numeric.c) — expand the exponent form.
376        None => match expand_scientific_literal(&s) {
377            SciExpanded::Expanded(plain) => match parse_decimal_literal(&plain) {
378                Some((unscaled, scale)) => Ok(Literal::Numeric { unscaled, scale }),
379                None if plain
380                    .split_once('.')
381                    .is_none_or(|(_, f)| u8::try_from(f.len()).is_ok()) =>
382                {
383                    Ok(Literal::NumericBig(plain))
384                }
385                None => s
386                    .parse::<f64>()
387                    .map(Literal::Float)
388                    .map_err(|_| format!("invalid numeric literal {s:?}")),
389            },
390            SciExpanded::Overflow => Err("value overflows numeric format".to_string()),
391            SciExpanded::NotScientific => s
392                .parse::<f64>()
393                .map(Literal::Float)
394                .map_err(|_| format!("invalid numeric literal {s:?}")),
395        },
396    }
397}
398
399fn parse_decimal_literal(s: &str) -> Option<(i128, u16)> {
400    let (int_part, frac_part) = match s.split_once('.') {
401        Some((i, f)) => (i, f),
402        None => (s, ""),
403    };
404    // v7.39 (round 271) — was u8::MAX. A literal with 256 decimal
405    // places fell out of the numeric path here, which is why
406    // `pg_typeof(1e-256)` answered double precision and a plain
407    // 256-place decimal aborted the query in the big-decimal converter.
408    if frac_part.len() > u16::MAX as usize {
409        return None;
410    }
411    let mut digits = String::with_capacity(int_part.len() + frac_part.len());
412    digits.push_str(int_part);
413    digits.push_str(frac_part);
414    let mantissa: i128 = digits.parse().ok()?;
415    #[allow(clippy::cast_possible_truncation)]
416    Some((mantissa, frac_part.len() as u16))
417}
418
419/// `jsonb_to_record` / `jsonb_to_recordset` (+ `json_` variants) — the
420/// record-returning JSON functions that take a `AS alias(col type, …)`
421/// column-definition list in FROM position.
422fn is_json_to_record_name(s: &str) -> bool {
423    s.eq_ignore_ascii_case("jsonb_to_recordset")
424        || s.eq_ignore_ascii_case("jsonb_to_record")
425        // v7.39 (read01 jsonfuncs.c) — the populate family with an AS
426        // column-definition list desugars identically (the record base
427        // argument only carries the type; a non-NULL base's field
428        // defaults are a recorded delta, RD-6).
429        || s.eq_ignore_ascii_case("json_populate_record")
430        || s.eq_ignore_ascii_case("jsonb_populate_record")
431        || s.eq_ignore_ascii_case("json_populate_recordset")
432        || s.eq_ignore_ascii_case("jsonb_populate_recordset")
433        || s.eq_ignore_ascii_case("json_to_recordset")
434        || s.eq_ignore_ascii_case("json_to_record")
435}
436
437impl Parser {
438    /// Whether what follows an identifier ends an index key, which is how
439    /// an operator class is told from anything else in that position.
440    fn opclass_position_follows(next: Option<&Token>) -> bool {
441        match next {
442            // `ASC` / `DESC` have their own tokens; matching them as
443            // identifiers named "asc" / "desc" — which the first version of
444            // this did — never fires, and `(c text_pattern_ops DESC)` (which
445            // PG18.4 accepts, verified) went on failing to parse.
446            Some(Token::Comma | Token::RParen | Token::Asc | Token::Desc) => true,
447            Some(Token::Ident(w)) => {
448                w.eq_ignore_ascii_case("nulls") || w.eq_ignore_ascii_case("collate")
449            }
450            _ => false,
451        }
452    }
453}
454
455fn is_vector_opclass_name(name: &str) -> bool {
456    let lc = name.to_ascii_lowercase();
457    matches!(
458        lc.as_str(),
459        "vector_cosine_ops"
460            | "vector_l2_ops"
461            | "vector_ip_ops"
462            | "halfvec_cosine_ops"
463            | "halfvec_l2_ops"
464            | "halfvec_ip_ops"
465            | "sq8_cosine_ops"
466            | "sq8_l2_ops"
467            | "sq8_ip_ops"
468            // pg_trgm — trigram operator class. SPG's GIN index
469            // already uses tsvector tokens; trigram-style LIKE
470            // pattern matching still routes through a sequential
471            // scan, but the opclass name is accepted so PG schemas
472            // load.
473            | "gin_trgm_ops"
474            | "gist_trgm_ops"
475            // PG built-in btree opclasses occasionally appear in
476            // pg_dump output for column types with multiple
477            // sort orders (text_pattern_ops, varchar_pattern_ops,
478            // bpchar_pattern_ops).
479            | "text_pattern_ops"
480            | "varchar_pattern_ops"
481            | "bpchar_pattern_ops"
482            | "int4_ops"
483            | "int8_ops"
484            | "text_ops"
485    )
486}
487
488#[derive(Debug, Clone, PartialEq, Eq)]
489pub struct ParseError {
490    pub message: String,
491    /// Index into the token stream where parsing tripped. Not a byte offset.
492    /// v7.39 (read01 round 95) — the byte/char position is NOT stored here: a
493    /// field would grow every `Result<_, ParseError>` slot on the deeply
494    /// recursive parse stack and tip the nesting-budget frame cliff. PG's
495    /// 1-based char position is recovered on the cold error path by
496    /// [`syntax_error_position`], which re-tokenizes to map this token index.
497    pub token_pos: usize,
498}
499
500impl fmt::Display for ParseError {
501    /// v7.39 (round 322/V24) — the message ALONE. It used to be prefixed
502    /// with `parse error at token #N: `, which PG has no equivalent of:
503    /// the message bodies are already PG's verbatim (`LIMIT must not be
504    /// negative`, `invalid input syntax for type bigint: "abc"`), and the
505    /// prefix was SPG's internal token index leaking into every one of
506    /// them. `token_pos` stays a field — the wire recovers PG's 1-based
507    /// character position from it for the ErrorResponse `P`.
508    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509        f.write_str(&self.message)
510    }
511}
512
513impl From<LexError> for ParseError {
514    fn from(e: LexError) -> Self {
515        Self {
516            message: format!("lex: {e}"),
517            token_pos: 0,
518        }
519    }
520}
521
522/// v7.9.30 — parse a single expression (no trailing junk). Used by
523/// the engine to re-hydrate stored partial-index / unique-index
524/// predicates from their canonical Display form. The same Pratt
525/// parser the statement path uses; this entry point just skips the
526/// statement dispatch.
527pub fn parse_expression(input: &str) -> Result<Expr, ParseError> {
528    let (tokens, offsets) = lexer::tokenize_with_offsets(input, lexer::Dialect::PG)
529        .map_err(|e| shape_lex_error(&e, input))?;
530    let mut p = Parser::new(tokens);
531    let expr = p
532        .parse_expr(0)
533        .and_then(|e| p.expect_eof().map(|()| e))
534        .map_err(|e| shape_syntax_error(e, input, &offsets))?;
535    Ok(expr)
536}
537
538/// Parse exactly one statement, swallow an optional trailing `;`, and require
539/// the token stream to end there. PG string semantics.
540pub fn parse_statement(input: &str) -> Result<Statement, ParseError> {
541    parse_statement_with(input, lexer::Dialect::PG)
542}
543
544/// v7.22 (round-13 T3) — dialect-aware entry: `backslash_escapes`
545/// selects MySQL-style string lexing (see `lexer::tokenize_with`).
546/// The engine threads its session flag through here.
547pub fn parse_statement_with(input: &str, dialect: lexer::Dialect) -> Result<Statement, ParseError> {
548    let (tokens, offsets, merges) =
549        lexer::tokenize_with_merges(input, dialect).map_err(|e| shape_lex_error(&e, input))?;
550    // v7.39.2 — the grammar follows "is this MySQL", the lexer follows
551    // "does backslash escape". They used to be one flag, and a session
552    // that turned escapes off lost the grammar with them.
553    let mut p = Parser::new_with_dialect(tokens, dialect.speaks_mysql)
554        .with_source(input, &offsets)
555        .with_merges(merges);
556    let stmt = (|| {
557        let stmt = p.parse_one_statement()?;
558        if matches!(p.peek(), Token::Semicolon) {
559            p.advance();
560        }
561        p.expect_eof()?;
562        Ok(stmt)
563    })()
564    .map_err(|e: ParseError| shape_syntax_error(e, input, &offsets))?;
565    Ok(stmt)
566}
567
568/// v7.39 (round 340, V56) — PG has exactly two syntax-error wordings:
569/// `syntax error at or near "<token>"` and `syntax error at end of input`
570/// (measured on 18.4 across a dozen shapes). SPG wrote its own per-site
571/// prose — `expected identifier, got Eof`, `unexpected token From in
572/// expression`, `expected end of input, got Ident("with")` — which named
573/// internal token types and, in the Debug forms, leaked the parser's own
574/// enum into a message clients read.
575///
576/// Applied once on the way out, so every construction site is covered and
577/// the token named is the one the error itself points at. Messages whose
578/// bodies are already PG's verbatim (`LIMIT must not be negative`,
579/// `invalid input syntax for type bigint: "abc"`) are left alone — those
580/// are PG's own errors, not its syntax error.
581fn shape_syntax_error(e: ParseError, input: &str, offsets: &[usize]) -> ParseError {
582    if !(e.message.starts_with("expected ") || e.message.starts_with("unexpected token ")) {
583        return e;
584    }
585    let message = match offending_lexeme(input, offsets, e.token_pos) {
586        Some(tok) => alloc::format!("syntax error at or near \"{tok}\""),
587        None => "syntax error at end of input".into(),
588    };
589    ParseError {
590        message,
591        token_pos: e.token_pos,
592    }
593}
594
595/// v7.39 (round 340, V56) — a lexer-level failure the way PG words it.
596/// Measured on 18.4: `unterminated quoted string at or near "'abc"`,
597/// `unterminated quoted identifier at or near ""abc"`, `unterminated /*
598/// comment at or near "/* x"` — the quoted part runs from the opening
599/// delimiter to the end of the input. SPG reported its own internal
600/// shape instead (`unterminated string literal at byte 7`), which named
601/// a byte offset no client can use.
602fn shape_lex_error(e: &lexer::LexError, input: &str) -> ParseError {
603    use lexer::LexErrorKind as K;
604    let from_here = input.get(e.pos..).map(str::trim_end).unwrap_or("");
605    let message = match &e.kind {
606        K::UnterminatedString => {
607            alloc::format!("unterminated quoted string at or near \"{from_here}\"")
608        }
609        K::UnterminatedQuotedIdent => {
610            alloc::format!("unterminated quoted identifier at or near \"{from_here}\"")
611        }
612        K::UnterminatedBlockComment => {
613            alloc::format!("unterminated /* comment at or near \"{from_here}\"")
614        }
615        // PG has no "unknown character" error of its own — the character
616        // is skipped and the parser reports the next token. SPG stops at
617        // the character itself and names it, which is the same shape.
618        K::UnknownChar(c) => alloc::format!("syntax error at or near \"{c}\""),
619        // The number-literal kinds already carry PG's `at or near` form.
620        other => alloc::format!(
621            "{}",
622            lexer::LexError {
623                kind: other.clone(),
624                pos: e.pos,
625            }
626        ),
627    };
628    ParseError {
629        message,
630        token_pos: 0,
631    }
632}
633
634/// The offending token exactly as it appears in the input, or `None` at
635/// end of input. PG echoes the source spelling — a lower-case `frm`
636/// reports as `frm`, not as a canonicalised keyword.
637fn offending_lexeme<'a>(input: &'a str, offsets: &[usize], token_pos: usize) -> Option<&'a str> {
638    let start = *offsets.get(token_pos)?;
639    if start >= input.len() {
640        return None;
641    }
642    let end = offsets
643        .get(token_pos + 1)
644        .copied()
645        .unwrap_or(input.len())
646        .min(input.len());
647    let seg = input.get(start..end)?.trim();
648    if seg.is_empty() {
649        return None;
650    }
651    // A quoted literal / identifier keeps its inner spaces; anything else
652    // ends at the first whitespace (the segment runs to the NEXT token's
653    // start, which may swallow a comment).
654    if seg.starts_with('\'') || seg.starts_with('"') || seg.starts_with('`') {
655        Some(seg)
656    } else {
657        seg.split_whitespace().next()
658    }
659}
660
661/// v7.39 (read01 round 95) — recover PG's 1-based CHARACTER error position for
662/// a [`ParseError::token_pos`]. Kept off the `ParseError` struct (and so off
663/// every recursive `Result` slot) to protect the nesting-budget frame cliff:
664/// this re-tokenizes `input` on the cold error path to map the failing token
665/// index to its start byte, then to a character offset. The dialect
666/// must match the parse that produced `token_pos` (it barely shifts offsets,
667/// but stay consistent). Returns `None` when the index has no offset or the
668/// byte isn't a char boundary. The wire attaches it as the ErrorResponse `P`.
669#[must_use]
670pub fn syntax_error_position(
671    input: &str,
672    dialect: lexer::Dialect,
673    token_pos: usize,
674) -> Option<usize> {
675    let (_, offsets) = lexer::tokenize_with_offsets(input, dialect).ok()?;
676    let byte_off = *offsets.get(token_pos)?;
677    if byte_off > input.len() || !input.is_char_boundary(byte_off) {
678        return None;
679    }
680    Some(input[..byte_off].chars().count() + 1)
681}
682
683struct Parser {
684    tokens: Vec<Token>,
685    pos: usize,
686    /// v7.39 (round 274) — the session's dialect, carried by the same
687    /// signal that drives string-literal escaping: `SET sql_mode` (only
688    /// MySQL clients and mysqldump preambles emit it) turns it on,
689    /// `SET standard_conforming_strings` (every pg_dump preamble) turns
690    /// it off. Needed here because the two dialects disagree about what
691    /// `REAL` means — see the type mapping below.
692    mysql_dialect: bool,
693    /// v7.30.2 (mailrs round-25 ask 2) — live nesting depth of the
694    /// mutually recursive expr/select parsers. Bounded so a deeply
695    /// nested input returns a parse error instead of overflowing
696    /// the stack (embed hosts die on overflow — it is an abort,
697    /// not a catchable error).
698    nest_depth: usize,
699    /// TABLESAMPLE lowering channel: the table-ref parser pushes a
700    /// `random() < p/100` predicate here; the enclosing SELECT
701    /// drains the list after its WHERE parses and ANDs the
702    /// predicates in. parse_bare_select save/restores around its
703    /// FROM+WHERE so nested selects only drain their own.
704    pending_sample_preds: Vec<Expr>,
705    /// v7.38.19 — the target of a `SELECT … INTO <table>`, carried out
706    /// of `parse_bare_select` (which returns a `SelectStatement` and has
707    /// nowhere to put it) to the caller that lowers the pair to the CTAS
708    /// node. `bool` is `TEMP`.
709    pending_select_into: Option<(String, bool)>,
710    /// v7.39 (round 691) — collation lowering channel, the same shape as
711    /// `pending_sample_preds` above. `expr COLLATE "name"` is ORDERING
712    /// information, and `ast::OrderBy` is where this parser keeps ordering
713    /// information (`desc`, `nulls_first`); the alternative — a new `Expr`
714    /// variant — puts a new arm on `eval_expr`, which this repo has
715    /// measured to overflow the debug stack. So while an ORDER BY KEY is
716    /// being parsed the postfix loop drops the name here instead of
717    /// refusing it, and the key's parser takes it.
718    ///
719    /// Only inside an ORDER BY key: everywhere else an unperformable
720    /// collation still errors, because accepting one at a COMPARISON and
721    /// ignoring it is the defect F36 exists to close.
722    in_order_by_key: bool,
723    order_key_collation: Option<String>,
724    /// POSITION(sub IN str) — while parsing the needle, the IN
725    /// keyword is the argument separator, not a membership test.
726    /// The postfix loop leaves IN unconsumed when this is set.
727    suppress_in_tail: bool,
728    /// Index of the token the last `advance()` returned — see
729    /// [`Parser::consumed_pos`].
730    last_consumed: usize,
731    /// v7.39 (round 506) — the statement's own text and the byte each token
732    /// starts at, so a MySQL projection item can report the SOURCE TEXT
733    /// MariaDB reports: `SELECT a  +  b` names its column `a  +  b`,
734    /// spacing and all. Only filled for a MySQL session — a PG one names
735    /// columns from the parsed shape and pays nothing for this.
736    src: Option<(String, Vec<usize>)>,
737    /// v7.39.3 — (token index, first-segment byte length) for every
738    /// string literal the lexer built by implicit concatenation.
739    merges: Vec<(usize, usize)>,
740}
741
742/// Max expr/select parser nesting (parens, subqueries, CASE, …).
743/// Real SQL nests a few dozen levels at the extreme. Each nesting level
744/// costs a parse_expr→parse_unary→parse_atom frame chain, so the budget
745/// exists to turn a deep statement into a catchable parse ERROR: a stack
746/// overflow is an abort, and in the server it does not fail one query, it
747/// takes the process down and every other connection with it.
748///
749/// v7.39 (round 507) — measured, because the figure here used to be a
750/// guess ("over 10 KiB in debug … comfortably inside a 2 MiB worker stack
751/// in BOTH debug and release"), and the debug half of that is wrong by
752/// more than an order of magnitude:
753///
754///   * RELEASE, on a 2 MiB worker stack: every recursive shape reaches
755///     this budget and errors. Verified against a live server for nested
756///     derived tables, parens, calls, CASE, IN-subqueries, scalar
757///     subqueries, NOT and unary minus — the server stayed up through all
758///     of them. This is the contract that matters, and it holds.
759///   * DEBUG: nested derived tables cost roughly 235 KiB of stack PER
760///     LEVEL, so parsing alone aborts around 35 levels on an 8 MiB stack
761///     and executing aborts around 8 inside a test thread. The budget is
762///     simply unreachable there, which is why a deep-nesting test has to
763///     ask for a large stack of its own — see `nesting_budget_errors_at`
764///     in the parser tests.
765/// v7.39 (round 541) — the pg_catalog relations SPG synthesises, in
766/// one place.
767///
768/// There were two copies of this fact: a curated list, used for BARE
769/// names, and — in `try_peek_meta_qualified` — no list at all, which
770/// rewrote `pg_catalog.<anything>` to `__spg_pg_<anything>` and left
771/// the engine to complain about a view it could not materialise. So
772/// writing the schema qualifier CHANGED THE ANSWER: `pg_stat_activity`
773/// had rows, `pg_catalog.pg_stat_activity` was an error.
774///
775/// PG puts `pg_catalog` at the implicit front of every search_path, so
776/// the two spellings name the same relation and must resolve the same
777/// way. Names NOT here (`pg_stat_activity`, `pg_locks`,
778/// `pg_stat_statements`, `pg_statio_user_tables`) route through the
779/// meta_view_result path under their own names and must not be
780/// rewritten; a name that is neither reaches the ordinary resolver,
781/// which reports that the relation does not exist — PG's answer.
782const SYNTHESISED_PG_CATALOGS: &[&str] = &[
783    "pg_am",
784    "pg_attrdef",
785    "pg_attribute",
786    "pg_cast",
787    "pg_db_role_setting",
788    "pg_conversion",
789    "pg_default_acl",
790    "pg_shadow",
791    "pg_sequences",
792    "pg_range",
793    "pg_partitioned_table",
794    "pg_language",
795    "pg_group",
796    "pg_authid",
797    "pg_class",
798    "pg_collation",
799    "pg_constraint",
800    "pg_database",
801    "pg_depend",
802    "pg_amop",
803    "pg_amproc",
804    "pg_opclass",
805    "pg_opfamily",
806    // v7.39 (read01 round 50) — COMMENT ON store, PG's pg_description.
807    "pg_description",
808    "pg_enum",
809    "pg_extension",
810    // v7.39 (round 541) — pg_dump reads it for every relation of kind
811    // 'f'. SPG has no foreign tables, so it is empty, which is also
812    // what PG reports on a database that has none.
813    "pg_foreign_table",
814    // v7.39 (round 541) — the empty-by-truth family; see
815    // EMPTY_PG_CATALOGS in spg-engine::system_catalog.
816    "pg_event_trigger",
817    "pg_file_settings",
818    "pg_foreign_data_wrapper",
819    "pg_foreign_server",
820    "pg_hba_file_rules",
821    "pg_ident_file_mappings",
822    "pg_init_privs",
823    "pg_parameter_acl",
824    "pg_prepared_xacts",
825    "pg_publication_namespace",
826    "pg_publication_rel",
827    "pg_publication_tables",
828    "pg_replication_origin",
829    "pg_replication_origin_status",
830    "pg_seclabel",
831    "pg_seclabels",
832    "pg_shdepend",
833    "pg_shdescription",
834    "pg_shmem_allocations",
835    "pg_shmem_allocations_numa",
836    "pg_shseclabel",
837    "pg_statistic_ext_data",
838    "pg_stats_ext",
839    "pg_stats_ext_exprs",
840    "pg_subscription_rel",
841    "pg_transform",
842    "pg_user_mapping",
843    "pg_user_mappings",
844    "pg_index",
845    "pg_indexes",
846    "pg_inherits",
847    // v7.39 (round 650) — the text-search catalogs SPG can fill
848    // honestly. `pg_ts_config_map` is deliberately NOT here: it maps
849    // token types to dictionaries and SPG has no token-type model,
850    // the same gap that leaves `ts_token_type` / `ts_debug` unbuilt.
851    "pg_ts_config",
852    "pg_ts_config_map",
853    "pg_ts_dict",
854    "pg_ts_parser",
855    "pg_ts_template",
856    "pg_matviews",
857    "pg_namespace",
858    // v7.39 (round 621)
859    "pg_operator",
860    "pg_policies",
861    "pg_policy",
862    "pg_proc",
863    "pg_publication",
864    "pg_replication_slots",
865    "pg_roles",
866    // v7.39 (round 143) — the rewrite-rule listing view.
867    // v7.39 (round 312) — and the rule catalogue itself, which
868    // `pg_get_ruledef(oid)` resolves against.
869    "pg_rewrite",
870    "pg_rules",
871    "pg_sequence",
872    "pg_settings",
873    "pg_stat_archiver",
874    "pg_stat_bgwriter",
875    "pg_stat_checkpointer",
876    "pg_stat_database",
877    "pg_stat_io",
878    "pg_stat_progress_analyze",
879    "pg_auth_members",
880    "pg_stat_progress_create_index",
881    "pg_stat_progress_vacuum",
882    "pg_stat_replication",
883    "pg_stat_slru",
884    "pg_stat_subscription_stats",
885    "pg_stat_user_functions",
886    "pg_stat_user_indexes",
887    "pg_stat_user_tables",
888    "pg_stat_wal",
889    "pg_prepared_statements",
890    "pg_largeobject",
891    "pg_largeobject_metadata",
892    "pg_statistic",
893    "pg_statistic_ext",
894    // v7.38.18 — the readable view over pg_statistic.
895    "pg_stats",
896    "pg_subscription",
897    "pg_tables",
898    "pg_tablespace",
899    // v7.39 (round 502) — the timezone catalogues. SPG resolved
900    // named zones correctly but could not list them, so a client
901    // populating a timezone picker got "relation does not exist".
902    "pg_timezone_abbrevs",
903    "pg_timezone_names",
904    "pg_trigger",
905    "pg_type",
906    "pg_user",
907    "pg_views",
908];
909
910const MAX_NEST_DEPTH: usize = 64;
911
912/// Stack accounting for the nesting budget, test-only.
913///
914/// `MAX_NEST_DEPTH` is a fixed count calibrated against a frame size
915/// that MOVES: a compiler upgrade grew the parser's debug frames and
916/// silently ate the margin until `nesting_budget_errors_cleanly` went
917/// from erroring cleanly to aborting on a stack overflow. A count
918/// cannot notice that on its own, so the budget is measured here and
919/// held to a ceiling.
920///
921/// The reading has to come from a helper whose OWN frame is the same at
922/// every call: debug slot placement does not follow source order, so a
923/// local's address inside the function under test is not that
924/// function's frame boundary. Two earlier probes were wrong that way —
925/// one read `&self.nest_depth`, which is the `Parser`'s address and
926/// never moves at all.
927#[cfg(test)]
928mod frame_meter {
929    extern crate std;
930    use std::cell::Cell;
931
932    // Per-THREAD, not global. `cargo test` runs tests in parallel and
933    // plenty of them parse nested expressions, so shared statics get
934    // stack addresses from several threads at once and the subtraction
935    // below turns into noise — it read 229,772 bytes per level that way,
936    // while passing when the test was run on its own.
937    std::thread_local! {
938        static AT_LO: Cell<usize> = const { Cell::new(0) };
939        static AT_HI: Cell<usize> = const { Cell::new(0) };
940    }
941
942    pub(super) const SAMPLE_LO: usize = 4;
943    pub(super) const SAMPLE_HI: usize = 24;
944
945    #[inline(never)]
946    pub(super) fn record(depth: usize) {
947        let anchor = 0u8;
948        let at = core::ptr::from_ref(&anchor) as usize;
949        if depth == SAMPLE_LO {
950            AT_LO.with(|c| c.set(at));
951        } else if depth == SAMPLE_HI {
952            AT_HI.with(|c| c.set(at));
953        }
954    }
955
956    /// Bytes of stack one nesting level costs, averaged over the span.
957    pub(super) fn bytes_per_level() -> usize {
958        let lo = AT_LO.with(Cell::get);
959        let hi = AT_HI.with(Cell::get);
960        assert!(lo > 0 && hi > 0, "meter never sampled: lo={lo} hi={hi}");
961        assert!(lo > hi, "stack grew upwards? lo={lo} hi={hi}");
962        (lo - hi) / (SAMPLE_HI - SAMPLE_LO)
963    }
964
965    pub(super) fn reset() {
966        AT_LO.with(|c| c.set(0));
967        AT_HI.with(|c| c.set(0));
968    }
969}
970
971/// v7.39 (read01 geo_ops.c) — prefix `@@` desugar target, out-of-line so
972/// the constructor's temporaries stay off `parse_unary`'s recursion frame.
973#[inline(never)]
974fn build_center_call(e: Expr) -> Expr {
975    Expr::FunctionCall {
976        name: alloc::string::String::from("center"),
977        args: alloc::vec![e],
978    }
979}
980
981/// Max consecutive binary operators at ONE precedence level
982/// (`a OR b OR c …`, `1+1+1…`). The chain builds iteratively at
983/// parse time but evaluates and drops recursively — depth beyond
984/// this overflows 2 MiB worker stacks (debug eval frames run
985/// multiple KiB). `IN (…)` lists are flat and unaffected.
986const MAX_BINARY_CHAIN: usize = 256;
987
988/// v7.22 (round-13 gap 5) — the kind keyword after `CONSTRAINT
989/// <name>` in a CREATE TABLE column list. FOREIGN KEY is not here:
990/// it keeps its dedicated path (`parse_table_level_fk`).
991enum NamedTableConstraintKind {
992    Check,
993    Unique,
994    PrimaryKey,
995    Exclude,
996}
997
998impl Parser {
999    fn new(tokens: Vec<Token>) -> Self {
1000        Self::new_with_dialect(tokens, false)
1001    }
1002
1003    fn new_with_dialect(tokens: Vec<Token>, mysql_dialect: bool) -> Self {
1004        Self {
1005            tokens,
1006            mysql_dialect,
1007            in_order_by_key: false,
1008            order_key_collation: None,
1009            pos: 0,
1010            nest_depth: 0,
1011            pending_sample_preds: Vec::new(),
1012            pending_select_into: None,
1013            suppress_in_tail: false,
1014            last_consumed: 0,
1015            src: None,
1016            merges: Vec::new(),
1017        }
1018    }
1019
1020    /// Hand the parser the text it is parsing, for [`Parser::source_span`].
1021    fn with_source(mut self, input: &str, offsets: &[usize]) -> Self {
1022        if self.mysql_dialect {
1023            self.src = Some((input.to_string(), offsets.to_vec()));
1024        }
1025        self
1026    }
1027
1028    /// v7.39.3 — the implicit-concatenation log from the lexer, so a
1029    /// merged literal can still be LABELLED by its first segment the way
1030    /// MySQL 9.7.2 labels it.
1031    fn with_merges(mut self, merges: Vec<(usize, usize)>) -> Self {
1032        if self.mysql_dialect {
1033            self.merges = merges;
1034        }
1035        self
1036    }
1037
1038    /// The byte length of the first segment of the literal at `tok`, when
1039    /// that literal was built by implicit concatenation.
1040    fn merged_first_len(&self, tok: usize) -> Option<usize> {
1041        self.merges
1042            .iter()
1043            .find(|(k, _)| *k == tok)
1044            .map(|(_, len)| *len)
1045    }
1046
1047    /// The source text spanning tokens `start ..= end`, trimmed.
1048    ///
1049    /// The offsets are token STARTS, so the span runs to the start of the
1050    /// token after `end` and gives back the whitespace between them —
1051    /// trimming is what makes `a + b FROM t` end at `b`.
1052    fn source_span(&self, start: usize, end: usize) -> Option<&str> {
1053        let (text, offsets) = self.src.as_ref()?;
1054        let from = *offsets.get(start)?;
1055        let to = *offsets.get(end + 1)?;
1056        text.get(from..to).map(str::trim_end)
1057    }
1058
1059    /// v7.30.2 (mailrs round-25 ask 2) — bump the expr/select
1060    /// nesting depth, erroring out cleanly past the budget.
1061    fn enter_nested(&mut self) -> Result<(), ParseError> {
1062        self.nest_depth += 1;
1063        #[cfg(test)]
1064        frame_meter::record(self.nest_depth);
1065        if self.nest_depth > MAX_NEST_DEPTH {
1066            self.nest_depth -= 1;
1067            return Err(self.err(alloc::format!(
1068                "statement nests deeper than {MAX_NEST_DEPTH} levels"
1069            )));
1070        }
1071        Ok(())
1072    }
1073
1074    fn peek(&self) -> &Token {
1075        // tokens always ends with Eof; pos is clamped in advance().
1076        &self.tokens[self.pos]
1077    }
1078
1079    fn advance(&mut self) -> Token {
1080        let t = mem::replace(&mut self.tokens[self.pos], Token::Eof);
1081        self.last_consumed = self.pos;
1082        if self.pos + 1 < self.tokens.len() {
1083            self.pos += 1;
1084        }
1085        t
1086    }
1087
1088    /// v7.39 (round 340, V56) — the index of the token `advance()` just
1089    /// returned. It was computed as `pos - 1`, which is wrong at both
1090    /// ends: `advance()` parks on the final Eof rather than running off
1091    /// the end (so `SELECT * FROM` named `FROM` where PG says `at end of
1092    /// input`), and after backtracking `pos` is no longer one past the
1093    /// token that failed. Recorded by `advance()` itself instead.
1094    const fn consumed_pos(&self) -> usize {
1095        self.last_consumed
1096    }
1097
1098    fn err(&self, message: String) -> ParseError {
1099        ParseError {
1100            message,
1101            token_pos: self.pos,
1102        }
1103    }
1104
1105    /// v7.39.3 — like [`Parser::err`] but pointing at a token the caller
1106    /// names rather than at the current one.
1107    ///
1108    /// The position is not decoration on the MySQL wire: its syntax-error
1109    /// sentence quotes the source from there to the end of the statement,
1110    /// so an error raised after the construct it is about quotes nothing.
1111    fn err_at(&self, token_pos: usize, message: String) -> ParseError {
1112        ParseError { message, token_pos }
1113    }
1114
1115    fn expect_eof(&self) -> Result<(), ParseError> {
1116        if matches!(self.peek(), Token::Eof) {
1117            Ok(())
1118        } else {
1119            Err(self.err(format!("expected end of input, got {:?}", self.peek())))
1120        }
1121    }
1122
1123    /// v7.14.0 — swallow every token up to (but not including) the
1124    /// next semicolon / EOF. Used by the dump-noise dispatcher
1125    /// to consume `COMMENT ON …`, `GRANT …`, `LOCK TABLES …`,
1126    /// etc. without modeling each grammar.
1127    /// v7.39 (read01 round 50) — `COMMENT ON <kind> <name> IS { 'text' | NULL }`.
1128    /// Kinds SPG stores by name: TABLE / COLUMN / INDEX / VIEW / SEQUENCE /
1129    /// SCHEMA / TYPE / DATABASE / FUNCTION. Anything else (and the multi-word
1130    /// `CONSTRAINT c ON t` / `MATERIALIZED VIEW` forms) keeps the old
1131    /// swallow-as-no-op behaviour so a pg_dump tail still loads.
1132    fn parse_comment_on(&mut self) -> Result<Statement, ParseError> {
1133        let start = self.pos;
1134        self.advance(); // COMMENT
1135        if !matches!(self.peek(), Token::On) {
1136            self.pos = start;
1137            self.consume_until_statement_boundary();
1138            return Ok(Statement::Empty);
1139        }
1140        self.advance(); // ON
1141        let kind = match self.peek() {
1142            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
1143            Token::Table => "table".into(),
1144            _ => {
1145                self.consume_until_statement_boundary();
1146                return Ok(Statement::Empty);
1147            }
1148        };
1149        if !matches!(
1150            kind.as_str(),
1151            "table"
1152                | "column"
1153                | "index"
1154                | "view"
1155                | "sequence"
1156                | "schema"
1157                | "type"
1158                | "database"
1159                | "function"
1160        ) {
1161            self.consume_until_statement_boundary();
1162            return Ok(Statement::Empty);
1163        }
1164        self.advance(); // the kind keyword
1165        // The object name. ⚠️ `expect_ident_like` strips a leading
1166        // `<schema>.` qualifier and returns only the trailing ident (SPG is
1167        // single-schema), which would turn `COMMENT ON COLUMN t.c` into just
1168        // `c`. Read the dotted parts from raw tokens instead, then let a
1169        // 3-part `schema.t.c` drop its leading schema like everywhere else.
1170        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
1171        loop {
1172            match self.advance() {
1173                Token::Ident(s) | Token::QuotedIdent(s) => parts.push(s),
1174                other if unreserved_keyword_text(&other).is_some() => {
1175                    parts.push(unreserved_keyword_text(&other).unwrap());
1176                }
1177                other => {
1178                    return Err(ParseError {
1179                        message: alloc::format!("expected identifier, got {other:?}"),
1180                        token_pos: self.consumed_pos(),
1181                    });
1182                }
1183            }
1184            if matches!(self.peek(), Token::Dot) {
1185                self.advance();
1186            } else {
1187                break;
1188            }
1189        }
1190        // COLUMN wants `table.column`; every other kind wants a bare name.
1191        let want = if kind == "column" { 2 } else { 1 };
1192        while parts.len() > want {
1193            parts.remove(0);
1194        }
1195        let name = parts.join(".");
1196        // v7.39 (round 710) — `COMMENT ON FUNCTION f(int, text) IS …`.
1197        // pg_dump writes the SIGNATURE, and the paren list was a syntax
1198        // error here — a dump carrying one function comment failed to
1199        // restore. The list is consumed (the comment store keys by name;
1200        // overload-precise comments are the function-predicate follow-up).
1201        if matches!(self.peek(), Token::LParen)
1202            && matches!(
1203                kind.as_str(),
1204                "function" | "procedure" | "aggregate" | "routine"
1205            )
1206        {
1207            let mut depth = 0usize;
1208            loop {
1209                match self.advance() {
1210                    Token::LParen => depth += 1,
1211                    Token::RParen => {
1212                        depth -= 1;
1213                        if depth == 0 {
1214                            break;
1215                        }
1216                    }
1217                    Token::Eof => {
1218                        return Err(self.err(alloc::string::String::from(
1219                            "unterminated argument list in COMMENT ON",
1220                        )));
1221                    }
1222                    _ => {}
1223                }
1224            }
1225        }
1226        // `IS`
1227        if !matches!(self.peek(), Token::Is) {
1228            self.expect_keyword_ident("is")?;
1229        } else {
1230            self.advance();
1231        }
1232        let comment = match self.peek() {
1233            Token::Null => {
1234                self.advance();
1235                None
1236            }
1237            _ => Some(self.expect_string_literal()?),
1238        };
1239        Ok(Statement::CommentOn {
1240            kind,
1241            name,
1242            comment,
1243        })
1244    }
1245
1246    /// v7.39 (read01 round 57) — `GRANT <privs> ON <obj> TO <roles> [WITH GRANT
1247    /// OPTION]` / `REVOKE [GRANT OPTION FOR] <privs> ON <obj> FROM <roles>
1248    /// [CASCADE|RESTRICT]`.
1249    ///
1250    /// TABLE privileges are the real ones (stored, enforced, introspectable).
1251    /// Every other object class — SCHEMA / SEQUENCE / DATABASE / FUNCTION / …,
1252    /// and the no-ON `GRANT role TO role` membership form — parses into
1253    /// `GrantObject::Other` and no-ops in the engine, so a pg_dump that grants
1254    /// on them still restores.
1255    fn parse_grant_or_revoke(&mut self, grant: bool) -> Result<Statement, ParseError> {
1256        self.advance(); // GRANT / REVOKE
1257        // REVOKE's optional `GRANT OPTION FOR` prefix.
1258        let mut grant_option = false;
1259        if !grant
1260            && self.peek_keyword_ident("grant")
1261            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("option"))
1262        {
1263            self.advance(); // GRANT
1264            self.advance(); // OPTION
1265            self.expect_keyword_ident("for")?;
1266            grant_option = true;
1267        }
1268        // The privilege list: `ALL [PRIVILEGES] [(cols)]`, or comma-separated
1269        // words each with an optional COLUMN list.
1270        let mut privileges: Vec<GrantPriv> = Vec::new();
1271        if matches!(self.peek(), Token::All) {
1272            self.advance();
1273            if self.peek_keyword_ident("privileges") {
1274                self.advance();
1275            }
1276            // `GRANT ALL (col) ON t TO r` — every column privilege, on that
1277            // column only.
1278            if matches!(self.peek(), Token::LParen) {
1279                let columns = self.parse_grant_column_list()?;
1280                privileges.push(GrantPriv {
1281                    word: "ALL".into(),
1282                    columns,
1283                });
1284            }
1285        } else {
1286            loop {
1287                // SELECT and INSERT lex as reserved tokens, so they never
1288                // reach `expect_ident_like` as plain idents; the rest
1289                // (UPDATE / DELETE / TRUNCATE / REFERENCES / TRIGGER /
1290                // MAINTAIN) are ordinary identifiers.
1291                let w = match self.peek() {
1292                    Token::Select => {
1293                        self.advance();
1294                        "SELECT".to_string()
1295                    }
1296                    Token::Insert => {
1297                        self.advance();
1298                        "INSERT".to_string()
1299                    }
1300                    // v7.39 (read01 round 60) — CREATE is a privilege word on a
1301                    // schema / database, and it lexes as a reserved token.
1302                    Token::Create => {
1303                        self.advance();
1304                        "CREATE".to_string()
1305                    }
1306                    // NOT upper-cased: in the no-ON shape (`GRANT devs TO
1307                    // alice`) these "privilege words" are ROLE NAMES, and a
1308                    // role name is case-sensitive. `priv_from_word` folds case
1309                    // itself when they really are privileges.
1310                    _ => self.expect_ident_like()?,
1311                };
1312                // v7.39 (read01 round 59) — the optional per-privilege COLUMN
1313                // list: `GRANT SELECT (a, b), INSERT (c) ON t TO dan`.
1314                let columns = if matches!(self.peek(), Token::LParen) {
1315                    self.parse_grant_column_list()?
1316                } else {
1317                    Vec::new()
1318                };
1319                privileges.push(GrantPriv { word: w, columns });
1320                if matches!(self.peek(), Token::Comma) {
1321                    self.advance();
1322                } else {
1323                    break;
1324                }
1325            }
1326        }
1327        // v7.39 (read01 round 58) — no ON clause at all = `GRANT devs TO alice`:
1328        // role MEMBERSHIP. The words parsed as "privileges" are the role names.
1329        if !matches!(self.peek(), Token::On) {
1330            let roles: Vec<String> = core::mem::take(&mut privileges)
1331                .into_iter()
1332                .map(|p| p.word)
1333                .collect();
1334            let grantees = self.parse_grantee_list(grant)?;
1335            // `WITH ADMIN OPTION` / `GRANTED BY x` — accepted, ignored (SPG has
1336            // no admin-option layer: a member cannot re-grant).
1337            self.consume_until_statement_boundary();
1338            return Ok(finish_grant(
1339                grant,
1340                GrantStatement {
1341                    privileges: Vec::new(),
1342                    object: GrantObject::Roles(roles),
1343                    grantees,
1344                    grant_option,
1345                },
1346            ));
1347        }
1348        self.advance(); // ON
1349        // An optional object-class keyword. `TABLE` (or no keyword at all) is
1350        // the enforced case; anything else parses and no-ops.
1351        let mut class = "TABLE";
1352        match self.peek() {
1353            Token::Table => {
1354                self.advance();
1355            }
1356            Token::All => {
1357                // v7.39 (read01 round 61) — `ALL TABLES IN SCHEMA x` expands to
1358                // every table at GRANT time, like PG. `ALL SEQUENCES/FUNCTIONS
1359                // IN SCHEMA` stay no-ops and keep their own object class.
1360                self.advance(); // ALL
1361                let kind = match self.peek() {
1362                    Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
1363                    // TABLES has its own token (SHOW TABLES owns it).
1364                    Token::Tables | Token::Table => "tables".to_string(),
1365                    _ => String::new(),
1366                };
1367                if !kind.is_empty() {
1368                    self.advance();
1369                }
1370                // `IN SCHEMA <name>`
1371                if matches!(self.peek(), Token::In) {
1372                    self.advance();
1373                    if self.peek_keyword_ident("schema") {
1374                        self.advance();
1375                        let _schema = self.expect_ident_like()?;
1376                    }
1377                }
1378                if kind != "tables" {
1379                    self.consume_until_statement_boundary();
1380                    return Ok(finish_grant(
1381                        grant,
1382                        GrantStatement {
1383                            privileges,
1384                            object: GrantObject::Other("ALL … IN SCHEMA".into()),
1385                            grantees: Vec::new(),
1386                            grant_option,
1387                        },
1388                    ));
1389                }
1390                let grantees = self.parse_grantee_list(grant)?;
1391                self.consume_until_statement_boundary();
1392                return Ok(finish_grant(
1393                    grant,
1394                    GrantStatement {
1395                        privileges,
1396                        object: GrantObject::AllTablesInSchema,
1397                        grantees,
1398                        grant_option,
1399                    },
1400                ));
1401            }
1402            Token::Ident(w) | Token::QuotedIdent(w) => {
1403                let lc = w.to_ascii_lowercase();
1404                // v7.39 (read01 round 60) — SEQUENCE / SCHEMA / DATABASE are
1405                // real objects with real ACLs now.
1406                if matches!(lc.as_str(), "sequence" | "schema" | "database") {
1407                    self.advance();
1408                    let mut names: Vec<String> = Vec::new();
1409                    loop {
1410                        let mut parts: Vec<String> = Vec::new();
1411                        loop {
1412                            parts.push(self.expect_ident_like()?);
1413                            if matches!(self.peek(), Token::Dot) {
1414                                self.advance();
1415                            } else {
1416                                break;
1417                            }
1418                        }
1419                        names.push(parts.pop().expect("at least one part"));
1420                        if matches!(self.peek(), Token::Comma) {
1421                            self.advance();
1422                        } else {
1423                            break;
1424                        }
1425                    }
1426                    let grantees = self.parse_grantee_list(grant)?;
1427                    let mut grant_option = grant_option;
1428                    if grant && self.peek_keyword_ident("with") {
1429                        self.advance();
1430                        self.expect_keyword_ident("grant")?;
1431                        self.expect_keyword_ident("option")?;
1432                        grant_option = true;
1433                    }
1434                    self.consume_until_statement_boundary();
1435                    let object = match lc.as_str() {
1436                        "sequence" => GrantObject::Sequences(names),
1437                        "schema" => GrantObject::Schemas(names),
1438                        _ => GrantObject::Databases(names),
1439                    };
1440                    return Ok(finish_grant(
1441                        grant,
1442                        GrantStatement {
1443                            privileges,
1444                            object,
1445                            grantees,
1446                            grant_option,
1447                        },
1448                    ));
1449                }
1450                // v7.39 (read01 round 61) — `ON FUNCTION f(int)` is real. SPG
1451                // keys functions by NAME, so the argument list parses and is
1452                // dropped (an overload set shares one ACL — recorded residual).
1453                if matches!(lc.as_str(), "function" | "procedure" | "routine") {
1454                    self.advance();
1455                    let mut names: Vec<(String, Option<Vec<String>>)> = Vec::new();
1456                    loop {
1457                        let mut parts: Vec<String> = Vec::new();
1458                        loop {
1459                            parts.push(self.expect_ident_like()?);
1460                            if matches!(self.peek(), Token::Dot) {
1461                                self.advance();
1462                            } else {
1463                                break;
1464                            }
1465                        }
1466                        let fname = parts.pop().expect("at least one part");
1467                        // v7.39 (read01 round 62) — the signature picks the
1468                        // overload, so it is captured.
1469                        let sig = if matches!(self.peek(), Token::LParen) {
1470                            Some(self.parse_function_signature_types()?)
1471                        } else {
1472                            None
1473                        };
1474                        names.push((fname, sig));
1475                        if matches!(self.peek(), Token::Comma) {
1476                            self.advance();
1477                        } else {
1478                            break;
1479                        }
1480                    }
1481                    let grantees = self.parse_grantee_list(grant)?;
1482                    self.consume_until_statement_boundary();
1483                    return Ok(finish_grant(
1484                        grant,
1485                        GrantStatement {
1486                            privileges,
1487                            object: GrantObject::Functions(names),
1488                            grantees,
1489                            grant_option,
1490                        },
1491                    ));
1492                }
1493                if matches!(
1494                    lc.as_str(),
1495                    "type"
1496                        | "domain"
1497                        | "language"
1498                        | "tablespace"
1499                        | "large"
1500                        | "foreign"
1501                        | "parameter"
1502                ) {
1503                    self.consume_until_statement_boundary();
1504                    return Ok(finish_grant(
1505                        grant,
1506                        GrantStatement {
1507                            privileges,
1508                            object: GrantObject::Other(lc.to_ascii_uppercase()),
1509                            grantees: Vec::new(),
1510                            grant_option,
1511                        },
1512                    ));
1513                }
1514                class = "TABLE";
1515            }
1516            _ => {}
1517        }
1518        let _ = class;
1519        // The table list. Schema-qualified names drop their qualifier (SPG is
1520        // single-schema) — but read the dotted parts from raw tokens, since
1521        // `expect_ident_like` would silently swallow the leading part.
1522        let mut tables: Vec<String> = Vec::new();
1523        loop {
1524            let mut parts: Vec<String> = Vec::new();
1525            loop {
1526                parts.push(self.expect_ident_like()?);
1527                if matches!(self.peek(), Token::Dot) {
1528                    self.advance();
1529                } else {
1530                    break;
1531                }
1532            }
1533            tables.push(parts.pop().expect("at least one part"));
1534            if matches!(self.peek(), Token::Comma) {
1535                self.advance();
1536            } else {
1537                break;
1538            }
1539        }
1540        let grantees = self.parse_grantee_list(grant)?;
1541        if grant && self.peek_keyword_ident("with") {
1542            self.advance();
1543            self.expect_keyword_ident("grant")?;
1544            self.expect_keyword_ident("option")?;
1545            grant_option = true;
1546        }
1547        // REVOKE's trailing CASCADE / RESTRICT — SPG has no dependent grants
1548        // to cascade to (no re-granting), so both are accepted and ignored.
1549        if !grant && (self.peek_keyword_ident("cascade") || self.peek_keyword_ident("restrict")) {
1550            self.advance();
1551        }
1552        Ok(finish_grant(
1553            grant,
1554            GrantStatement {
1555                privileges,
1556                object: GrantObject::Tables(tables),
1557                grantees,
1558                grant_option,
1559            },
1560        ))
1561    }
1562
1563    /// v7.39 (read01 round 62) — the argument TYPES in a function signature:
1564    /// `(int, text)` or `(x int, y text)` (PG accepts either). Returns the type
1565    /// words; the caller normalises them into a signature key.
1566    fn parse_function_signature_types(&mut self) -> Result<Vec<String>, ParseError> {
1567        self.advance(); // (
1568        let mut types: Vec<String> = Vec::new();
1569        if matches!(self.peek(), Token::RParen) {
1570            self.advance();
1571            return Ok(types);
1572        }
1573        loop {
1574            // Collect the words of one argument up to a comma / close paren.
1575            let mut words: Vec<String> = Vec::new();
1576            loop {
1577                match self.peek() {
1578                    Token::Comma | Token::RParen | Token::Eof => break,
1579                    _ => {}
1580                }
1581                let tok = self.advance();
1582                match tok {
1583                    Token::Ident(w) | Token::QuotedIdent(w) => words.push(w),
1584                    other => {
1585                        if let Some(w) = unreserved_keyword_text(&other) {
1586                            words.push(w);
1587                        }
1588                    }
1589                }
1590            }
1591            // `name TYPE` or a bare `TYPE`. Several of PG's type names are
1592            // themselves several words (`double precision`, `character
1593            // varying`, `timestamp with time zone`), so "two words means the
1594            // first is a parameter name" reads the type off `f(double
1595            // precision)` as `precision`. v7.39 (round 282): recognise the
1596            // multi-word spellings first — a leading word that STARTS one of
1597            // them is part of the type, not a name.
1598            let joined = words.join(" ");
1599            let ty = if words.len() >= 2 && is_multiword_type_phrase(&joined) {
1600                joined
1601            } else if words.len() >= 2 {
1602                words[1..].join(" ")
1603            } else {
1604                words.first().cloned().unwrap_or_default()
1605            };
1606            types.push(ty);
1607            if matches!(self.peek(), Token::Comma) {
1608                self.advance();
1609            } else {
1610                break;
1611            }
1612        }
1613        if matches!(self.peek(), Token::RParen) {
1614            self.advance();
1615        }
1616        Ok(types)
1617    }
1618
1619    /// v7.39 (read01 round 59) — `( col, col, … )` after a privilege word.
1620    fn parse_grant_column_list(&mut self) -> Result<Vec<String>, ParseError> {
1621        self.advance(); // (
1622        let mut cols = Vec::new();
1623        loop {
1624            cols.push(self.expect_ident_like()?);
1625            if matches!(self.peek(), Token::Comma) {
1626                self.advance();
1627            } else {
1628                break;
1629            }
1630        }
1631        if !matches!(self.peek(), Token::RParen) {
1632            return Err(self.err(alloc::format!(
1633                "expected ')' to close the column list, got {:?}",
1634                self.peek()
1635            )));
1636        }
1637        self.advance(); // )
1638        Ok(cols)
1639    }
1640
1641    /// `TO <roles>` (grant) / `FROM <roles>` (revoke). An empty-string entry is
1642    /// PUBLIC.
1643    fn parse_grantee_list(&mut self, grant: bool) -> Result<Vec<String>, ParseError> {
1644        if grant {
1645            if matches!(self.peek(), Token::To) {
1646                self.advance();
1647            } else {
1648                self.expect_keyword_ident("to")?;
1649            }
1650        } else if matches!(self.peek(), Token::From) {
1651            self.advance();
1652        } else {
1653            self.expect_keyword_ident("from")?;
1654        }
1655        let mut grantees: Vec<String> = Vec::new();
1656        loop {
1657            // `GROUP name` is the legacy spelling of a plain role name.
1658            if self.peek_keyword_ident("group") {
1659                self.advance();
1660            }
1661            if self.peek_keyword_ident("public") {
1662                self.advance();
1663                grantees.push(String::new()); // PUBLIC
1664            } else {
1665                grantees.push(self.expect_ident_like()?);
1666            }
1667            if matches!(self.peek(), Token::Comma) {
1668                self.advance();
1669            } else {
1670                break;
1671            }
1672        }
1673        Ok(grantees)
1674    }
1675
1676    /// v7.39 (round 277) — `PREPARE <name> [(type, …)] AS <statement>`.
1677    /// The body keeps its `$N` placeholders; substitution happens at
1678    /// EXECUTE. The declared types are recorded for
1679    /// `pg_prepared_statements.parameter_types` but are not enforced —
1680    /// PG infers when the list is omitted, and SPG resolves the values
1681    /// at substitution time either way.
1682    fn parse_prepare(&mut self) -> Result<Statement, ParseError> {
1683        let start = self.pos;
1684        self.advance(); // PREPARE
1685        // v7.39 (round 278) — `PREPARE TRANSACTION '<gid>'` is 2PC, a
1686        // different statement that happens to share the keyword. PG
1687        // ships with `max_prepared_transactions = 0` and reports it
1688        // this way; SPG has no prepared-transaction registry, so the
1689        // same wording is the accurate answer rather than a dodge.
1690        // Round 277 turned this from a silent no-op into a confusing
1691        // "expected AS in PREPARE" parse error.
1692        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
1693            self.advance();
1694            let gid = match self.advance() {
1695                Token::String(g) => g,
1696                other => {
1697                    return Err(self.err(alloc::format!(
1698                        "expected a transaction identifier after PREPARE TRANSACTION, got {other:?}"
1699                    )));
1700                }
1701            };
1702            return Ok(Statement::PrepareTransaction(gid));
1703        }
1704        let name = self.expect_ident_like()?;
1705        let mut param_types = Vec::new();
1706        if matches!(self.peek(), Token::LParen) {
1707            self.advance();
1708            loop {
1709                let mut ty = self.expect_ident_like()?;
1710                // A parameterised type name (`numeric(10,2)`,
1711                // `varchar(20)`) keeps its argument list in the text.
1712                if matches!(self.peek(), Token::LParen) {
1713                    let mut depth = 0usize;
1714                    let mut buf = String::from("(");
1715                    loop {
1716                        match self.advance() {
1717                            Token::LParen => {
1718                                depth += 1;
1719                                if depth > 1 {
1720                                    buf.push('(');
1721                                }
1722                            }
1723                            Token::RParen => {
1724                                depth -= 1;
1725                                buf.push(')');
1726                                if depth == 0 {
1727                                    break;
1728                                }
1729                            }
1730                            Token::Comma => buf.push(','),
1731                            Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
1732                            Token::Eof => break,
1733                            _ => {}
1734                        }
1735                    }
1736                    ty.push_str(&buf);
1737                }
1738                // r1049 — `PREPARE p(bigint[]) AS …`: the sixth `[]`
1739                // position, same family as the parameter list above.
1740                let array_suffix = self.consume_array_suffix();
1741                ty.push_str(&array_suffix);
1742                param_types.push(ty);
1743                match self.peek() {
1744                    Token::Comma => {
1745                        self.advance();
1746                    }
1747                    Token::RParen => {
1748                        self.advance();
1749                        break;
1750                    }
1751                    other => {
1752                        return Err(self.err(alloc::format!(
1753                            "expected ',' or ')' in PREPARE parameter list, got {other:?}"
1754                        )));
1755                    }
1756                }
1757            }
1758        }
1759        if !matches!(self.peek(), Token::As) {
1760            return Err(self.err(alloc::format!(
1761                "expected AS in PREPARE, got {:?}",
1762                self.peek()
1763            )));
1764        }
1765        self.advance();
1766        let body = self.parse_one_statement()?;
1767        // The Parser holds tokens, not the source text, so the
1768        // statement PG reports in `pg_prepared_statements.statement`
1769        // is rebuilt from the AST rather than sliced from the input.
1770        let _ = start;
1771        let mut source = alloc::format!("PREPARE {}", crate::ast::quote_ident(&name));
1772        if !param_types.is_empty() {
1773            source.push_str(" (");
1774            source.push_str(&param_types.join(", "));
1775            source.push(')');
1776        }
1777        source.push_str(" AS ");
1778        source.push_str(&alloc::format!("{body}"));
1779        Ok(Statement::Prepare {
1780            name,
1781            param_types,
1782            body: alloc::boxed::Box::new(body),
1783            source,
1784        })
1785    }
1786
1787    /// v7.39 (round 277) — `EXECUTE <name> [(<expr>, …)]`.
1788    fn parse_execute(&mut self) -> Result<Statement, ParseError> {
1789        self.advance(); // EXECUTE
1790        let name = self.expect_ident_like()?;
1791        let mut args = Vec::new();
1792        if matches!(self.peek(), Token::LParen) {
1793            self.advance();
1794            if matches!(self.peek(), Token::RParen) {
1795                self.advance();
1796            } else {
1797                loop {
1798                    args.push(self.parse_expr(0)?);
1799                    match self.advance() {
1800                        Token::Comma => {}
1801                        Token::RParen => break,
1802                        other => {
1803                            return Err(self.err(alloc::format!(
1804                                "expected ',' or ')' in EXECUTE arguments, got {other:?}"
1805                            )));
1806                        }
1807                    }
1808                }
1809            }
1810        }
1811        Ok(Statement::Execute { name, args })
1812    }
1813
1814    /// v7.39 (round 277) — `DEALLOCATE {[PREPARE] <name> | ALL}`.
1815    /// v7.39 (round 278) — `CALL <proc>([args])`. There is no
1816    /// procedure catalog yet, so this reports PG's not-found error
1817    /// (with its HINT) rather than pretending the call ran.
1818    /// v7.39 (round 320, V53) — `DISCARD { ALL | PLANS | SEQUENCES | TEMP }`.
1819    /// Bare `DISCARD` is a syntax error in PG; so it is here.
1820    fn parse_discard(&mut self) -> Result<Statement, ParseError> {
1821        self.advance(); // DISCARD
1822        let target = match self.advance() {
1823            Token::All => DiscardTarget::All,
1824            Token::Ident(w) | Token::QuotedIdent(w) => match w.to_ascii_lowercase().as_str() {
1825                "all" => DiscardTarget::All,
1826                "plans" => DiscardTarget::Plans,
1827                "sequences" => DiscardTarget::Sequences,
1828                "temp" | "temporary" => DiscardTarget::Temp,
1829                other => {
1830                    return Err(self.err(format!(
1831                        "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1832                    )));
1833                }
1834            },
1835            other => {
1836                return Err(self.err(format!(
1837                    "expected ALL / PLANS / SEQUENCES / TEMP after DISCARD, got {other:?}"
1838                )));
1839            }
1840        };
1841        Ok(Statement::Discard(target))
1842    }
1843
1844    /// v7.39 (round 318, V51) — MySQL `KILL [HARD|SOFT] [CONNECTION|QUERY]
1845    /// <expr>`. MariaDB accepts an expression for the id (its own docs use
1846    /// `KILL connection_id()`), and the HARD / SOFT prefixes only pick how
1847    /// aggressively the server interrupts, which SPG does not distinguish.
1848    /// Bare `KILL <id>` means CONNECTION.
1849    fn parse_kill(&mut self) -> Result<Statement, ParseError> {
1850        self.advance(); // KILL
1851        let mut query_only = false;
1852        loop {
1853            // CONNECTION is a reserved keyword token (it also opens
1854            // `CREATE SUBSCRIPTION … CONNECTION '…'`), so it arrives as
1855            // `Token::Connection` rather than a bare ident.
1856            if matches!(self.peek(), Token::Connection) {
1857                self.advance();
1858                break;
1859            }
1860            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
1861                break;
1862            };
1863            match w.to_ascii_lowercase().as_str() {
1864                "hard" | "soft" => {
1865                    self.advance();
1866                }
1867                "query" => {
1868                    self.advance();
1869                    query_only = true;
1870                    break;
1871                }
1872                _ => break,
1873            }
1874        }
1875        let id = self.parse_expr(0)?;
1876        Ok(Statement::Kill {
1877            query_only,
1878            id: Box::new(id),
1879        })
1880    }
1881
1882    fn parse_call(&mut self) -> Result<Statement, ParseError> {
1883        self.advance(); // CALL
1884        let name = self.expect_ident_like()?;
1885        self.consume_until_statement_boundary();
1886        Ok(Statement::Call(name))
1887    }
1888
1889    fn parse_deallocate(&mut self) -> Result<Statement, ParseError> {
1890        self.advance(); // DEALLOCATE
1891        // PG accepts an optional noise `PREPARE` keyword here.
1892        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("prepare")) {
1893            self.advance();
1894        }
1895        if matches!(self.peek(), Token::All) {
1896            self.advance();
1897            return Ok(Statement::Deallocate(None));
1898        }
1899        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("all")) {
1900            self.advance();
1901            return Ok(Statement::Deallocate(None));
1902        }
1903        let name = self.expect_ident_like()?;
1904        Ok(Statement::Deallocate(Some(name)))
1905    }
1906
1907    fn consume_until_statement_boundary(&mut self) {
1908        loop {
1909            match self.peek() {
1910                Token::Semicolon | Token::Eof => return,
1911                _ => self.advance(),
1912            };
1913        }
1914    }
1915
1916    /// v7.38.19 — the database name a `CREATE DATABASE` names, skipping
1917    /// an `IF NOT EXISTS`. Consumes only the name; the collation scanner
1918    /// runs after it and eats the rest.
1919    fn scan_database_name(&mut self) -> Option<String> {
1920        // The caller has only PEEKED at `DATABASE`; step past it, or the
1921        // first identifier found is the keyword itself. It was, and
1922        // `pg_database` listed a database called `database`.
1923        if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case("database"))
1924        {
1925            self.advance();
1926        }
1927        for kw in ["if", "not", "exists"] {
1928            if matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w) if w.eq_ignore_ascii_case(kw))
1929            {
1930                self.advance();
1931            }
1932        }
1933        match self.peek().clone() {
1934            Token::Ident(w) | Token::QuotedIdent(w) => {
1935                self.advance();
1936                Some(w)
1937            }
1938            _ => None,
1939        }
1940    }
1941
1942    /// v7.38.18 — consume to the statement boundary like
1943    /// `consume_until_statement_boundary`, but pick out the collation a
1944    /// `CREATE DATABASE` asked for on the way.
1945    ///
1946    /// `LC_COLLATE 'de_DE.utf8'` and `LOCALE 'de_DE.utf8'` both count;
1947    /// `LC_CTYPE` does not, because SPG has no separate ctype and
1948    /// pretending to honour it would be the more misleading answer. An
1949    /// `=` between the keyword and the value is optional, as in PG.
1950    ///
1951    /// The whole statement used to be thrown away. Being single-database
1952    /// makes the NAME a no-op; it does not make the collation one.
1953    fn scan_database_collation_until_boundary(&mut self) -> Option<String> {
1954        let mut want_value = false;
1955        let mut found: Option<String> = None;
1956        loop {
1957            let tok = self.peek().clone();
1958            match &tok {
1959                Token::Semicolon | Token::Eof => break,
1960                Token::Ident(w) | Token::QuotedIdent(w)
1961                    if w.eq_ignore_ascii_case("lc_collate") || w.eq_ignore_ascii_case("locale") =>
1962                {
1963                    want_value = true;
1964                }
1965                Token::Eq if want_value => {}
1966                Token::String(v) if want_value => {
1967                    found = Some(v.clone());
1968                    want_value = false;
1969                }
1970                Token::Ident(v) | Token::QuotedIdent(v) if want_value => {
1971                    found = Some(v.clone());
1972                    want_value = false;
1973                }
1974                _ => want_value = false,
1975            }
1976            self.advance();
1977        }
1978        found
1979    }
1980
1981    /// v7.22 (round-13 T2) — consume to the statement boundary like
1982    /// `consume_until_statement_boundary`, but pick out the sequence
1983    /// name on the way: either `SEQUENCE NAME <ident>` (identity
1984    /// columns) or the first string literal (`nextval('<seq>')`).
1985    /// Schema qualifiers and `::regclass` casts are stripped.
1986    fn scan_sequence_name_until_boundary(&mut self) -> Option<String> {
1987        let mut seq: Option<String> = None;
1988        let mut after_sequence_kw = false;
1989        let mut after_name_kw = false;
1990        loop {
1991            match self.peek().clone() {
1992                Token::Semicolon | Token::Eof => break,
1993                Token::Ident(s) | Token::QuotedIdent(s) => {
1994                    if after_name_kw && seq.is_none() {
1995                        self.advance();
1996                        let mut name = s;
1997                        // `SEQUENCE NAME public.groups_id_seq` — keep
1998                        // the bare name, drop qualifiers.
1999                        while matches!(self.peek(), Token::Dot) {
2000                            self.advance();
2001                            if let Token::Ident(n) | Token::QuotedIdent(n) = self.advance() {
2002                                name = n;
2003                            }
2004                        }
2005                        seq = Some(name);
2006                        after_name_kw = false;
2007                        continue;
2008                    }
2009                    if after_sequence_kw && s.eq_ignore_ascii_case("name") {
2010                        after_name_kw = true;
2011                        after_sequence_kw = false;
2012                    } else {
2013                        after_sequence_kw = s.eq_ignore_ascii_case("sequence");
2014                    }
2015                    self.advance();
2016                }
2017                Token::String(s) => {
2018                    if seq.is_none() {
2019                        // `nextval('public.groups_id_seq'::regclass)`
2020                        let bare = s
2021                            .rsplit_once('.')
2022                            .map_or_else(|| s.clone(), |(_, b)| b.to_string());
2023                        seq = Some(bare);
2024                    }
2025                    self.advance();
2026                }
2027                _ => {
2028                    after_sequence_kw = false;
2029                    after_name_kw = false;
2030                    self.advance();
2031                }
2032            }
2033        }
2034        seq
2035    }
2036
2037    /// v7.39 (round 621) — is the next token the keyword `BY`?
2038    ///
2039    /// `pg_get_keywords()` classes `by` as `U` (unreserved), so it is a legal
2040    /// column, table and alias name — and SPG lexed it into a dedicated
2041    /// `Token::By`, which made it unusable as a name ANYWHERE. Of the seven
2042    /// two-letter keywords the lexer knew, this was the only one PG leaves
2043    /// unreserved (`as`, `in`, `on`, `or`, `to` are reserved and `is` is `T`).
2044    ///
2045    /// The token is gone; the three clauses that own the word — GROUP BY,
2046    /// ORDER BY, PARTITION BY — and the handful of other places that expect it
2047    /// ask this instead. Adding it to the unreserved-identifier table was not
2048    /// enough on its own: identifier positions that match the token shape
2049    /// directly (an index's column list, a table alias) never consult that
2050    /// table, so `CREATE INDEX … ON t(by)` and `FROM t AS by` still failed.
2051    /// Not lexing it as a keyword closes the whole class rather than the two
2052    /// positions that happened to be noticed.
2053    fn peek_is_by(&self) -> bool {
2054        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by"))
2055    }
2056
2057    /// v7.39 (round 621) — the optional `CASCADE` / `RESTRICT` trailer a DROP
2058    /// takes. Accepted and dropped: SPG tracks no dependents to cascade to,
2059    /// which is what `DROP TABLE` and `DROP INDEX` have done since v7.14.
2060    fn consume_drop_behaviour(&mut self) {
2061        if matches!(
2062            self.peek(),
2063            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") || s.eq_ignore_ascii_case("restrict")
2064        ) {
2065            self.advance();
2066        }
2067    }
2068
2069    fn expect_ident_like(&mut self) -> Result<String, ParseError> {
2070        let first = match self.advance() {
2071            Token::Ident(s) | Token::QuotedIdent(s) => s,
2072            // v7.37.43-T4 — PG-unreserved keywords are legal identifiers
2073            // per PG's `pg_get_keywords()` classification. SPG tokenizes
2074            // these as named variants for parsing leverage in the
2075            // contexts that own them (`RELEASE SAVEPOINT`, `SHOW name`,
2076            // `BEGIN`, etc.), but they MUST still be usable as table /
2077            // column / alias names in DDL+DML. Sentori migrations like
2078            // 0001_init.sql ship `release TEXT NOT NULL` in the events
2079            // table — the `events.release` column carries the release
2080            // identifier string. Pre-T4 this triggered "expected
2081            // identifier, got Release" and blocked every drop-in user
2082            // whose schema had a column / alias with one of these names.
2083            other if unreserved_keyword_text(&other).is_some() => {
2084                unreserved_keyword_text(&other).unwrap()
2085            }
2086            other => {
2087                return Err(ParseError {
2088                    message: format!("expected identifier, got {other:?}"),
2089                    token_pos: self.consumed_pos(),
2090                });
2091            }
2092        };
2093        // v7.14.0 — strip optional `<schema>.` prefix. PG dumps
2094        // qualify every name with `public.` (and pg_catalog.* for
2095        // functions); SPG is single-schema so we discard the
2096        // prefix and return only the trailing ident. Same shape
2097        // also handles MySQL `db.tbl` cross-database refs (SPG
2098        // ignores the db part).
2099        if matches!(self.peek(), Token::Dot) {
2100            self.advance();
2101            match self.advance() {
2102                Token::Ident(s) | Token::QuotedIdent(s) => return Ok(s),
2103                other if unreserved_keyword_text(&other).is_some() => {
2104                    return Ok(unreserved_keyword_text(&other).unwrap());
2105                }
2106                other => {
2107                    return Err(ParseError {
2108                        message: format!("expected identifier after '{first}.', got {other:?}"),
2109                        token_pos: self.consumed_pos(),
2110                    });
2111                }
2112            }
2113        }
2114        Ok(first)
2115    }
2116
2117    #[allow(clippy::too_many_lines)]
2118    fn parse_one_statement(&mut self) -> Result<Statement, ParseError> {
2119        // v7.14.0 — empty / comment-only / semicolon-only input
2120        // (after the lexer strips line + block + MySQL
2121        // conditional comments) lands as Statement::Empty.
2122        // pg_dump and mysqldump emit several wrappers that
2123        // collapse to nothing after stripping (`/*!40101 SET …
2124        // */;`, blank lines between statements); the engine
2125        // returns CommandOk no-op so the dump loads cleanly.
2126        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
2127            return Ok(Statement::Empty);
2128        }
2129        // v7.14.0 — pg_dump / mysqldump "noise" statements:
2130        // catalog / metadata DDL that has no behavioural effect
2131        // on SPG's single-schema, single-database, single-user
2132        // model. Consume the whole statement up to the next
2133        // semicolon / EOF and return Empty. This is broader than
2134        // the per-keyword DROP / SET / COMMENT arms but lets the
2135        // long tail of `LOCK TABLES`, `UNLOCK TABLES`, `GRANT`,
2136        // `REVOKE`, `ALTER OWNER TO`, `\restrict`, `\unrestrict`,
2137        // `BEGIN; COMMIT;` wrappers, etc. all pass through.
2138        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
2139            let lc = s.to_ascii_lowercase();
2140            // v7.39 (read01 round 50) — COMMENT ON is a real statement now.
2141            if lc == "comment" {
2142                return self.parse_comment_on();
2143            }
2144            // v7.39 (read01 round 57) — so is GRANT / REVOKE.
2145            if lc == "grant" || lc == "revoke" {
2146                return self.parse_grant_or_revoke(lc == "grant");
2147            }
2148            // v7.39 (round 277) — the SQL-level prepared-statement
2149            // surface is REAL now. It used to be accepted and dropped
2150            // on the theory that "real execution still happens via the
2151            // extended-query flow" — true only for a driver that uses
2152            // that flow; a plain SQL PREPARE / EXECUTE returned no
2153            // rows at all.
2154            if lc == "prepare" {
2155                return self.parse_prepare();
2156            }
2157            if lc == "execute" {
2158                return self.parse_execute();
2159            }
2160            if lc == "deallocate" {
2161                return self.parse_deallocate();
2162            }
2163            // v7.39 (round 278) — `CALL <proc>(<args>)` used to be
2164            // accepted and dropped, so an application's stored-procedure
2165            // invocation reported success and did nothing. SPG has no
2166            // procedure catalog, so every CALL names a procedure that
2167            // does not exist — which is exactly what PG says.
2168            if lc == "call" {
2169                return self.parse_call();
2170            }
2171            // v7.39 (round 318, V51) — MySQL `KILL`. Not dump noise: it
2172            // names one connection and acts on it.
2173            if lc == "kill" {
2174                return self.parse_kill();
2175            }
2176            if lc == "discard" {
2177                return self.parse_discard();
2178            }
2179            // v7.39 (round 696) — REASSIGN OWNED BY <role> [, …] TO <role>.
2180            // Still performs nothing; the roles are carried out so a name
2181            // that does not exist is refused, as PG18 refuses it.
2182            if lc == "reassign" {
2183                self.advance();
2184                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("owned")) {
2185                    self.advance();
2186                }
2187                if self.peek_is_by() {
2188                    self.advance();
2189                }
2190                // Only the roles BEFORE the TO are the ones that must
2191                // exist — `TO` names the new owner, which PG checks as
2192                // well, so both lists are collected.
2193                let mut names = self.take_comma_separated_names();
2194                if matches!(self.peek(), Token::To) {
2195                    self.advance();
2196                    names.extend(self.take_comma_separated_names());
2197                }
2198                self.consume_until_statement_boundary();
2199                return Ok(Statement::ValidateOnly {
2200                    kind: crate::ast::ValidateOnlyKind::RoleName,
2201                    names,
2202                });
2203            }
2204            // v7.39 (round 696) — SECURITY LABEL. PG18 refuses it
2205            // unconditionally with `no security label providers have been
2206            // loaded`, whatever object it names, because none is loaded.
2207            // SPG has none either; accepting it told the caller a label had
2208            // been applied when nothing anywhere records one.
2209            if lc == "security" {
2210                self.consume_until_statement_boundary();
2211                return Ok(Statement::ValidateOnly {
2212                    kind: crate::ast::ValidateOnlyKind::SecurityLabel,
2213                    names: Vec::new(),
2214                });
2215            }
2216            // v7.39.2 — `USE <db>` is a real statement now, and only in
2217            // the MySQL dialect. It used to be swallowed here with the
2218            // dump noise, so `USE myapp; SELECT DATABASE()` answered the
2219            // same constant it answered before — MySQL 9.7.2 answers
2220            // `myapp`. PostgreSQL has no USE at all, and pg_dump does not
2221            // emit one, but the swallow stays on that side: it was put
2222            // there for restores and taking it away is not this defect.
2223            if lc == "use" {
2224                if self.mysql_dialect {
2225                    self.advance();
2226                    let name = self.expect_ident_like()?;
2227                    return Ok(Statement::UseDatabase(name));
2228                }
2229                self.consume_until_statement_boundary();
2230                return Ok(Statement::Empty);
2231            }
2232            if is_dump_noise_statement(&lc) {
2233                self.consume_until_statement_boundary();
2234                return Ok(Statement::Empty);
2235            }
2236        }
2237        match self.peek() {
2238            Token::Select => self.parse_select_stmt(),
2239            // v7.37.17 (17.6 siblings) — a statement opening with a
2240            // parenthesized query group: `(SELECT … UNION …)
2241            // INTERSECT …`. parse_bare_select's group arm consumes
2242            // the parens; the select parser handles the outer chain
2243            // and tail.
2244            Token::LParen
2245                if matches!(
2246                    self.tokens.get(self.pos + 1),
2247                    Some(Token::Select | Token::LParen | Token::Values)
2248                ) =>
2249            {
2250                self.parse_select_stmt()
2251            }
2252            // v7.37.17 (17.6 siblings) — top-level bare VALUES
2253            // statement (`VALUES (1), (2) [ORDER BY …] [LIMIT …]`).
2254            // Lowers to the same UNION ALL chain the FROM-position
2255            // form uses, then reuses the shared SELECT tail.
2256            Token::Values => {
2257                self.advance(); // VALUES
2258                let mut head = self.parse_values_rows_body()?;
2259                self.parse_select_tail_into(&mut head)?;
2260                Ok(Statement::Select(head))
2261            }
2262            // SQL-standard `TABLE name` shorthand for
2263            // `SELECT * FROM name` — pg_dump never emits it, but
2264            // psql users and PG docs use it constantly. Set-op
2265            // chains and the ORDER BY/LIMIT tail compose like any
2266            // SELECT head.
2267            Token::Table
2268                if matches!(
2269                    self.tokens.get(self.pos + 1),
2270                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2271                ) =>
2272            {
2273                let mut head = self.parse_table_shorthand()?;
2274                self.parse_setop_chain_into(&mut head)?;
2275                self.parse_select_tail_into(&mut head)?;
2276                Ok(Statement::Select(head))
2277            }
2278            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2279            // body is a dollar-quoted plpgsql block (lexer already
2280            // collapsed `$$…$$` into a single Token::String).
2281            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2282            // real PlPgSqlBlock so the engine can EXECUTE it at
2283            // top level instead of silently swallowing. Pre-
2284            // v7.16.2 the parser threw the body away and the
2285            // engine returned CommandOk for the entire DO; that
2286            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2287            // $$` into a SEV-1 silent no-op (the IF + the rename
2288            // were both invisible — mailrs's migrate-042 didn't
2289            // actually run). Now the body parses + executes;
2290            // EmbeddedSql inside the block runs immediately
2291            // against the engine (not deferred — we're at top
2292            // level, not inside a trigger row-write loop).
2293            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2294                self.advance();
2295                let body_text = match self.advance() {
2296                    Token::String(s) => s,
2297                    other => {
2298                        return Err(self.err(alloc::format!(
2299                            "expected dollar-quoted body after DO, got {other:?}"
2300                        )));
2301                    }
2302                };
2303                // Optional `LANGUAGE <name>` trailer (idents only).
2304                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2305                    self.advance();
2306                    let _ = self.expect_ident_like()?;
2307                }
2308                // Parse the body — same shape CREATE FUNCTION
2309                // uses for trigger function bodies. If the body
2310                // doesn't parse cleanly we surface the error
2311                // (better than silent no-op).
2312                let block = parse_plpgsql_body(&body_text)?;
2313                Ok(Statement::DoBlock(block))
2314            }
2315            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2316            // WITH isn't a reserved token in our lexer — comes through
2317            // as `Token::Ident("with")` (case-insensitive).
2318            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2319                self.advance();
2320                self.parse_with_cte_then_select()
2321            }
2322            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2323            // an identifier — not a reserved keyword.
2324            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2325                self.advance();
2326                let mut analyze = false;
2327                let mut suggest = false;
2328                let mut costs_off = false;
2329                let mut buffers = false;
2330                let mut timing_off = false;
2331                let mut settings = false;
2332                let mut wal = false;
2333                let mut summary_off = false;
2334                let mut format = crate::ast::ExplainFormat::Text;
2335                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2336                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2337                // options are comma-separated. Booleans default to ON
2338                // when the value token is omitted (matches PG).
2339                if matches!(self.peek(), Token::LParen) {
2340                    self.advance();
2341                    loop {
2342                        let opt = match self.peek().clone() {
2343                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2344                            other => {
2345                                return Err(self.err(format!(
2346                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2347                                )));
2348                            }
2349                        };
2350                        self.advance();
2351                        if opt.eq_ignore_ascii_case("suggest") {
2352                            suggest = true;
2353                            // SUGGEST takes no explicit value today.
2354                        } else if opt.eq_ignore_ascii_case("costs") {
2355                            // PG syntax: `COSTS [ON | OFF]`. Default
2356                            // when value omitted is ON, so plain
2357                            // `COSTS` is a no-op. `COSTS OFF` flips.
2358                            // `ON` lexes to `Token::On` (reserved
2359                            // keyword in JOIN ... ON contexts); accept
2360                            // it alongside the bare Ident form so the
2361                            // grammar matches PG verbatim.
2362                            let value = match self.peek().clone() {
2363                                Token::On => {
2364                                    self.advance();
2365                                    true
2366                                }
2367                                Token::Ident(v) | Token::QuotedIdent(v)
2368                                    if v.eq_ignore_ascii_case("off") =>
2369                                {
2370                                    self.advance();
2371                                    false
2372                                }
2373                                Token::Ident(v) | Token::QuotedIdent(v)
2374                                    if v.eq_ignore_ascii_case("true") =>
2375                                {
2376                                    self.advance();
2377                                    true
2378                                }
2379                                _ => true,
2380                            };
2381                            costs_off = !value;
2382                        } else if opt.eq_ignore_ascii_case("analyze")
2383                            || opt.eq_ignore_ascii_case("analyse")
2384                        {
2385                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2386                            // Same default-ON rule as ANALYZE keyword form.
2387                            let value = match self.peek().clone() {
2388                                Token::On => {
2389                                    self.advance();
2390                                    true
2391                                }
2392                                Token::Ident(v) | Token::QuotedIdent(v)
2393                                    if v.eq_ignore_ascii_case("off") =>
2394                                {
2395                                    self.advance();
2396                                    false
2397                                }
2398                                Token::Ident(v) | Token::QuotedIdent(v)
2399                                    if v.eq_ignore_ascii_case("true") =>
2400                                {
2401                                    self.advance();
2402                                    true
2403                                }
2404                                _ => true,
2405                            };
2406                            analyze = value;
2407                        } else if opt.eq_ignore_ascii_case("buffers") {
2408                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2409                            let value = match self.peek().clone() {
2410                                Token::On => {
2411                                    self.advance();
2412                                    true
2413                                }
2414                                Token::Ident(v) | Token::QuotedIdent(v)
2415                                    if v.eq_ignore_ascii_case("off") =>
2416                                {
2417                                    self.advance();
2418                                    false
2419                                }
2420                                Token::Ident(v) | Token::QuotedIdent(v)
2421                                    if v.eq_ignore_ascii_case("true") =>
2422                                {
2423                                    self.advance();
2424                                    true
2425                                }
2426                                _ => true,
2427                            };
2428                            buffers = value;
2429                        } else if opt.eq_ignore_ascii_case("timing") {
2430                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2431                            // the measured wall-clock annotation.
2432                            let value = match self.peek().clone() {
2433                                Token::On => {
2434                                    self.advance();
2435                                    true
2436                                }
2437                                Token::Ident(v) | Token::QuotedIdent(v)
2438                                    if v.eq_ignore_ascii_case("off") =>
2439                                {
2440                                    self.advance();
2441                                    false
2442                                }
2443                                Token::Ident(v) | Token::QuotedIdent(v)
2444                                    if v.eq_ignore_ascii_case("true") =>
2445                                {
2446                                    self.advance();
2447                                    true
2448                                }
2449                                _ => true,
2450                            };
2451                            timing_off = !value;
2452                        } else if opt.eq_ignore_ascii_case("settings") {
2453                            settings = true;
2454                        } else if opt.eq_ignore_ascii_case("wal") {
2455                            wal = true;
2456                        } else if opt.eq_ignore_ascii_case("summary") {
2457                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2458                            // gates the trailing Planning/Execution Time
2459                            // lines now (was accept-and-no-op).
2460                            let value = match self.peek().clone() {
2461                                Token::On => {
2462                                    self.advance();
2463                                    true
2464                                }
2465                                Token::Ident(v) | Token::QuotedIdent(v)
2466                                    if v.eq_ignore_ascii_case("off") =>
2467                                {
2468                                    self.advance();
2469                                    false
2470                                }
2471                                Token::Ident(v) | Token::QuotedIdent(v)
2472                                    if v.eq_ignore_ascii_case("true") =>
2473                                {
2474                                    self.advance();
2475                                    true
2476                                }
2477                                _ => true,
2478                            };
2479                            summary_off = !value;
2480                        } else if opt.eq_ignore_ascii_case("verbose")
2481                            || opt.eq_ignore_ascii_case("format")
2482                        {
2483                            // v7.37.22 — accept-but-no-op the remaining
2484                            // PG options so EXPLAIN-using clients
2485                            // (pgAdmin / DataGrip) don't see syntax
2486                            // errors. FORMAT takes a value (text /
2487                            // json / yaml / xml); skip the next token
2488                            // if it's an ident.
2489                            if opt.eq_ignore_ascii_case("format") {
2490                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2491                                {
2492                                    self.advance();
2493                                    format = match v.to_ascii_lowercase().as_str() {
2494                                        "text" => crate::ast::ExplainFormat::Text,
2495                                        "json" => crate::ast::ExplainFormat::Json,
2496                                        "xml" => crate::ast::ExplainFormat::Xml,
2497                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2498                                        other => {
2499                                            return Err(self.err(format!(
2500                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2501                                                 supports text, json, xml, yaml"
2502                                            )));
2503                                        }
2504                                    };
2505                                }
2506                            } else {
2507                                // VERBOSE / SUMMARY take optional ON/OFF;
2508                                // consume if present.
2509                                if matches!(self.peek(), Token::On) {
2510                                    self.advance();
2511                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2512                                    self.peek().clone()
2513                                    && (v.eq_ignore_ascii_case("off")
2514                                        || v.eq_ignore_ascii_case("true"))
2515                                {
2516                                    self.advance();
2517                                    let _ = v;
2518                                }
2519                            }
2520                        } else {
2521                            return Err(self.err(format!(
2522                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2523                            )));
2524                        }
2525                        if matches!(self.peek(), Token::Comma) {
2526                            self.advance();
2527                            continue;
2528                        }
2529                        break;
2530                    }
2531                    if !matches!(self.peek(), Token::RParen) {
2532                        return Err(self.err(format!(
2533                            "expected ')' after EXPLAIN options, got {:?}",
2534                            self.peek()
2535                        )));
2536                    }
2537                    self.advance();
2538                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2539                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2540                {
2541                    self.advance();
2542                    analyze = true;
2543                }
2544                // v7.39 (round 224) — the body may open with WITH (CTEs);
2545                // route through the same CTE-then-SELECT path the top-level
2546                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2547                // too (PG explains INSERT / UPDATE / DELETE).
2548                let inner = match self.peek().clone() {
2549                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2550                        self.advance();
2551                        self.parse_with_cte_then_select()?
2552                    }
2553                    Token::Insert => self.parse_insert_stmt(false)?,
2554                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2555                        self.advance();
2556                        self.parse_update_after_keyword()?
2557                    }
2558                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2559                        self.advance();
2560                        self.parse_delete_after_keyword()?
2561                    }
2562                    _ => self.parse_select_stmt()?,
2563                };
2564                if !matches!(
2565                    inner,
2566                    Statement::Select(_)
2567                        | Statement::Insert(_)
2568                        | Statement::Update(_)
2569                        | Statement::Delete(_)
2570                ) {
2571                    return Err(self.err(format!(
2572                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2573                    )));
2574                }
2575                Ok(Statement::Explain(crate::ast::ExplainStatement {
2576                    analyze,
2577                    inner: Box::new(inner),
2578                    suggest,
2579                    costs_off,
2580                    buffers,
2581                    timing_off,
2582                    settings,
2583                    wal,
2584                    summary_off,
2585                    format,
2586                }))
2587            }
2588            Token::Create => self.parse_create_stmt(),
2589            Token::Insert => self.parse_insert_stmt(false),
2590            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2591            // spelling; route to the same handler. DESC is the
2592            // reserved ORDER BY token, so it gets its own arm.
2593            Token::Ident(s)
2594                if s.eq_ignore_ascii_case("describe")
2595                    && matches!(
2596                        self.tokens.get(self.pos + 1),
2597                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2598                    ) =>
2599            {
2600                self.advance();
2601                let table = self.expect_ident_like()?;
2602                Ok(Statement::ShowColumns(table))
2603            }
2604            Token::Desc
2605                if matches!(
2606                    self.tokens.get(self.pos + 1),
2607                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2608                ) =>
2609            {
2610                self.advance();
2611                let table = self.expect_ident_like()?;
2612                Ok(Statement::ShowColumns(table))
2613            }
2614            // `COPY table [(cols)] TO STDOUT` — the export half of
2615            // pg_dump's COPY pair (the FROM stdin half rides the
2616            // embed import path). Options need a format design and
2617            // error honestly.
2618            Token::Ident(s)
2619                if s.eq_ignore_ascii_case("copy")
2620                    && matches!(
2621                        self.tokens.get(self.pos + 1),
2622                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2623                    ) =>
2624            {
2625                self.advance(); // COPY
2626                let table = self.expect_ident_like()?;
2627                let columns = if matches!(self.peek(), Token::LParen) {
2628                    self.advance();
2629                    let mut cols = alloc::vec![self.expect_ident_like()?];
2630                    while matches!(self.peek(), Token::Comma) {
2631                        self.advance();
2632                        cols.push(self.expect_ident_like()?);
2633                    }
2634                    if !matches!(self.peek(), Token::RParen) {
2635                        return Err(self.err(format!(
2636                            "expected ')' after COPY column list, got {:?}",
2637                            self.peek()
2638                        )));
2639                    }
2640                    self.advance();
2641                    Some(cols)
2642                } else {
2643                    None
2644                };
2645                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2646                // endpoint. (FROM STDIN still rides the wire/import path —
2647                // its data arrives out of band.)
2648                if matches!(self.peek(), Token::From)
2649                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2650                {
2651                    self.advance(); // FROM
2652                    let Token::String(path) = self.advance() else {
2653                        unreachable!()
2654                    };
2655                    let options = self.parse_copy_to_options()?;
2656                    return Ok(Statement::CopyFromFile {
2657                        table,
2658                        columns,
2659                        path,
2660                        options,
2661                    });
2662                }
2663                if !matches!(self.peek(), Token::To) {
2664                    return Err(self.err(format!(
2665                        "COPY: only TO STDOUT is supported here (FROM stdin \
2666                         rides the import path); got {:?}",
2667                        self.peek()
2668                    )));
2669                }
2670                self.advance();
2671                if matches!(self.peek(), Token::String(_)) {
2672                    let Token::String(path) = self.advance() else { unreachable!() };
2673                    let options = self.parse_copy_to_options()?;
2674                    return Ok(Statement::CopyToFile {
2675                        table,
2676                        columns,
2677                        query: None,
2678                        path,
2679                        options,
2680                    });
2681                }
2682                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2683                    return Err(self.err(format!(
2684                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2685                        self.peek()
2686                    )));
2687                }
2688                self.advance();
2689                let options = self.parse_copy_to_options()?;
2690                Ok(Statement::CopyTo {
2691                    table,
2692                    columns,
2693                    query: None,
2694                    options,
2695                })
2696            }
2697            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2698            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2699            // result set is streamed in COPY format (PG's query form).
2700            Token::Ident(s)
2701                if s.eq_ignore_ascii_case("copy")
2702                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2703            {
2704                self.advance(); // COPY
2705                self.advance(); // (
2706                let query = self.parse_select_stmt()?;
2707                if !matches!(self.peek(), Token::RParen) {
2708                    return Err(self.err(format!(
2709                        "expected ')' after COPY query, got {:?}",
2710                        self.peek()
2711                    )));
2712                }
2713                self.advance(); // )
2714                if !matches!(self.peek(), Token::To) {
2715                    return Err(self.err(format!(
2716                        "COPY (query): only TO STDOUT is supported, got {:?}",
2717                        self.peek()
2718                    )));
2719                }
2720                self.advance();
2721                if matches!(self.peek(), Token::String(_)) {
2722                    let Token::String(path) = self.advance() else { unreachable!() };
2723                    let options = self.parse_copy_to_options()?;
2724                    return Ok(Statement::CopyToFile {
2725                        table: String::new(),
2726                        columns: None,
2727                        query: Some(alloc::boxed::Box::new(query)),
2728                        path,
2729                        options,
2730                    });
2731                }
2732                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2733                    return Err(self.err(format!(
2734                        "COPY (query): TO supports STDOUT only, got {:?}",
2735                        self.peek()
2736                    )));
2737                }
2738                self.advance();
2739                let options = self.parse_copy_to_options()?;
2740                Ok(Statement::CopyTo {
2741                    table: String::new(),
2742                    columns: None,
2743                    query: Some(alloc::boxed::Box::new(query)),
2744                    options,
2745                })
2746            }
2747            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2748            // Shares the INSERT body; the replace flag lowers it
2749            // onto ON CONFLICT DO UPDATE with an empty assignment
2750            // list (engine: replace the whole row).
2751            Token::Ident(s)
2752                if s.eq_ignore_ascii_case("replace")
2753                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2754            {
2755                self.parse_insert_stmt(true)
2756            }
2757            Token::Begin => {
2758                self.advance();
2759                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2760                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2761                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2762                // is consumed first, then the trailing modes — including the
2763                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2764                // WORK/TRANSACTION). The explicit level, when present, rides the
2765                // statement so `exec_begin` applies it for this transaction.
2766                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2767                {
2768                    self.advance();
2769                }
2770                let iso = self.parse_isolation_level_clauses()?;
2771                Ok(Statement::Begin(iso))
2772            }
2773            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2774            // for BEGIN. START is contextual in PG too; pattern-match
2775            // on the ident here. Iso clauses are parse-and-ignored,
2776            // same as BEGIN above.
2777            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2778                self.advance();
2779                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2780                {
2781                    return Err(self.err(alloc::format!(
2782                        "expected TRANSACTION after START, got {:?}",
2783                        self.peek()
2784                    )));
2785                }
2786                self.advance();
2787                let iso = self.parse_isolation_level_clauses()?;
2788                Ok(Statement::Begin(iso))
2789            }
2790            Token::Commit => {
2791                self.advance();
2792                // PG: `COMMIT [WORK | TRANSACTION]`.
2793                if let Token::Ident(w) = self.peek()
2794                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2795                {
2796                    self.advance();
2797                }
2798                Ok(Statement::Commit)
2799            }
2800            // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2801            // COMMIT synonym; pgbench's builtin tpcb-like script closes
2802            // every transaction with `END;` and the drop-in aborted on
2803            // it. Only reachable at statement start (CASE … END lives
2804            // inside expressions), so no ambiguity.
2805            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2806                self.advance();
2807                if let Token::Ident(w) = self.peek()
2808                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2809                {
2810                    self.advance();
2811                }
2812                Ok(Statement::Commit)
2813            }
2814            Token::Rollback => {
2815                self.advance();
2816                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2817                // savepoint without ending the transaction. Bare
2818                // `ROLLBACK` drops the whole TX.
2819                if matches!(self.peek(), Token::To) {
2820                    self.advance();
2821                    if matches!(self.peek(), Token::Savepoint) {
2822                        self.advance();
2823                    }
2824                    let name = self.expect_ident_like()?;
2825                    Ok(Statement::RollbackToSavepoint(name))
2826                } else {
2827                    Ok(Statement::Rollback)
2828                }
2829            }
2830            Token::Savepoint => {
2831                self.advance();
2832                let name = self.expect_ident_like()?;
2833                Ok(Statement::Savepoint(name))
2834            }
2835            Token::Release => {
2836                self.advance();
2837                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2838                // is optional in standard SQL.
2839                if matches!(self.peek(), Token::Savepoint) {
2840                    self.advance();
2841                }
2842                let name = self.expect_ident_like()?;
2843                Ok(Statement::ReleaseSavepoint(name))
2844            }
2845            Token::Show => {
2846                self.advance();
2847                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2848                // v6.1.2 promoted TABLES to a reserved keyword (for
2849                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2850                // arrives as `Token::Tables` rather than a bare ident.
2851                // USERS / COLUMNS remain bare idents.
2852                let target = match self.advance() {
2853                    Token::Tables => "tables".to_string(),
2854                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2855                    // keyword token; recognise it as the SHOW CREATE
2856                    // dispatch keyword too.
2857                    Token::Create => "create".to_string(),
2858                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2859                    // keyword too; let SHOW INDEX FROM parse.
2860                    Token::Index => "index".to_string(),
2861                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2862                    // reserved (used in aggregate function calls);
2863                    // recognise it here so the parser dispatches
2864                    // to ShowParameter("all") — the engine returns
2865                    // the curated parameter inventory.
2866                    Token::All => "all".to_string(),
2867                    // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2868                    // spelling for the size of the diagnostics area.
2869                    // MySQL-dialect only: PostgreSQL 18.4 answers this
2870                    // phrase with `syntax error at or near "("`, and a
2871                    // PG session must keep getting exactly that rather
2872                    // than a message about an unknown parameter.
2873                    // `COUNT` arrives as a bare ident; the `(*)` and the
2874                    // trailing keyword are consumed here so the whole
2875                    // form reaches the engine as one parameter name.
2876                    Token::Ident(ref c)
2877                        if self.mysql_dialect
2878                            && c.eq_ignore_ascii_case("count")
2879                            && matches!(self.peek(), Token::LParen) =>
2880                    {
2881                        self.advance();
2882                        if matches!(self.peek(), Token::Star) {
2883                            self.advance();
2884                        }
2885                        if matches!(self.peek(), Token::RParen) {
2886                            self.advance();
2887                        }
2888                        match self.advance() {
2889                            Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2890                                return Ok(Statement::ShowParameter(
2891                                    "count(*) warnings".to_string(),
2892                                ));
2893                            }
2894                            other => {
2895                                return Err(self.err(format!(
2896                                    "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2897                                )));
2898                            }
2899                        }
2900                    }
2901                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2902                    other => {
2903                        return Err(self.err(format!(
2904                            "expected SHOW target, got {other:?}"
2905                        )));
2906                    }
2907                };
2908                match target.as_str() {
2909                    "tables" => Ok(Statement::ShowTables),
2910                    "users" => Ok(Statement::ShowUsers),
2911                    // v7.38 轴 4 — `SHOW transaction_isolation`
2912                    // returns the currently-selected isolation level.
2913                    "transaction_isolation" => Ok(Statement::ShowParameter(
2914                        "transaction_isolation".to_string(),
2915                    )),
2916                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2917                    // TABLE <t>` returns a 2-column row: (Table,
2918                    // Create Table). mysqldump emits this for every
2919                    // table at scrape time; without it the dump
2920                    // round-trip stalls.
2921                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2922                    // FROM <t>` (also spelled `SHOW INDEX` and
2923                    // `SHOW KEYS`). admin / mysqldump probes use
2924                    // it to list per-table indexes.
2925                    "indexes" | "index" | "keys" => {
2926                        if !matches!(self.peek(), Token::From) {
2927                            return Err(self.err(format!(
2928                                "expected FROM after SHOW INDEXES, got {:?}",
2929                                self.peek()
2930                            )));
2931                        }
2932                        self.advance();
2933                        let table = self.expect_ident_like()?;
2934                        Ok(Statement::ShowIndexes(table))
2935                    }
2936                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2937                    // `SHOW VARIABLES`. Both return a 2-column row
2938                    // set listing server-side state; clients probe
2939                    // them at connect time.
2940                    "status" => Ok(Statement::ShowStatus),
2941                    "variables" => {
2942                        // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2943                        if matches!(self.peek(), Token::Like) {
2944                            self.advance();
2945                            let pat = match self.advance() {
2946                                Token::String(p) => p,
2947                                other => {
2948                                    return Err(self.err(format!(
2949                                        "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2950                                    )));
2951                                }
2952                            };
2953                            return Ok(Statement::ShowVariablesLike(pat));
2954                        }
2955                        Ok(Statement::ShowVariables)
2956                    }
2957                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2958                    "processlist" => Ok(Statement::ShowProcesslist),
2959                    "create" => {
2960                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2961                        // TABLE is supported in v7.17.
2962                        let kind = match self.advance() {
2963                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2964                            Token::Table => "table".to_string(),
2965                            other => {
2966                                return Err(self.err(format!(
2967                                    "expected TABLE after SHOW CREATE, got {other:?}"
2968                                )));
2969                            }
2970                        };
2971                        if !kind.eq_ignore_ascii_case("table") {
2972                            return Err(self.err(format!(
2973                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2974                            )));
2975                        }
2976                        let name = self.expect_ident_like()?;
2977                        Ok(Statement::ShowCreateTable(name))
2978                    }
2979                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2980                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2981                    // it to populate the database selector at connect
2982                    // time; without it `mysql -p` errors before the
2983                    // first user query.
2984                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2985                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2986                    // keyword on its own; it lands here as a bare
2987                    // ident. Returning all publications + their
2988                    // scope summary.
2989                    "publications" => Ok(Statement::ShowPublications),
2990                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2991                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2992                    "columns" => {
2993                        if !matches!(self.peek(), Token::From) {
2994                            return Err(self.err(format!(
2995                                "expected FROM after SHOW COLUMNS, got {:?}",
2996                                self.peek()
2997                            )));
2998                        }
2999                        self.advance();
3000                        let table = self.expect_ident_like()?;
3001                        Ok(Statement::ShowColumns(table))
3002                    }
3003                    // v7.38 轴 4 surface — `SHOW <param>` for any
3004                    // remaining session / preset parameter name
3005                    // (server_version, search_path, client_encoding,
3006                    // …). The engine's ShowParameter handler does the
3007                    // dispatch; unrecognised names error there with
3008                    // a pointer to pg_settings, not at parse time —
3009                    // so a driver that issues `SHOW spam_setting`
3010                    // gets a clear runtime error instead of a
3011                    // confusing "unknown SHOW target".
3012                    other => {
3013                        // v7.38 (read01 P3.20) — a custom namespaced GUC
3014                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
3015                        // consume the dotted tail so it round-trips with
3016                        // `SET app.foo` / `current_setting('app.foo')`.
3017                        let mut full = other.to_string();
3018                        while matches!(self.peek(), Token::Dot) {
3019                            self.advance();
3020                            let seg = self.expect_ident_like()?;
3021                            full.push('.');
3022                            full.push_str(&seg.to_ascii_lowercase());
3023                        }
3024                        Ok(Statement::ShowParameter(full))
3025                    }
3026                }
3027            }
3028            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
3029            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
3030            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
3031            // arrived as a bare ident; tokenising it dedicatedly
3032            // keeps the dispatch tree small.
3033            Token::Drop => {
3034                self.advance();
3035                match self.peek() {
3036                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
3037                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
3038                    // around DROP ROLE cleanup. SPG has no role-owner
3039                    // model, so consume to boundary as a no-op.
3040                    Token::Ident(s) | Token::QuotedIdent(s)
3041                        if s.eq_ignore_ascii_case("owned") =>
3042                    {
3043                        // v7.39 (round 696) — still a no-op (SPG has no
3044                        // role-owner model), but the ROLE is carried out so
3045                        // the engine can refuse one that does not exist,
3046                        // which is what PG18 does.
3047                        self.advance();
3048                        if self.peek_is_by() {
3049                            self.advance();
3050                        }
3051                        let names = self.take_comma_separated_names();
3052                        self.consume_until_statement_boundary();
3053                        Ok(Statement::ValidateOnly {
3054                            kind: crate::ast::ValidateOnlyKind::RoleName,
3055                            names,
3056                        })
3057                    }
3058                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3059                    // It drops only a TEMPORARY table, and name resolution
3060                    // already prefers the session's own, so the keyword is
3061                    // consumed and the ordinary DROP TABLE path runs.
3062                    Token::Ident(s) | Token::QuotedIdent(s)
3063                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3064                    {
3065                        self.advance();
3066                        if !matches!(self.peek(), Token::Table) {
3067                            return Err(self.err(alloc::format!(
3068                                "expected TABLE after DROP TEMPORARY, got {:?}",
3069                                self.peek()
3070                            )));
3071                        }
3072                        self.parse_drop_table_after_keyword()
3073                    }
3074                    Token::Publication => {
3075                        self.advance();
3076                        // v7.39 (round 754, F31-B4) — the round-753
3077                        // audit probe tripped over the missing
3078                        // `IF EXISTS` here (syntax error).
3079                        let if_exists = self.consume_if_exists();
3080                        let name = self.expect_ident_or_string()?;
3081                        Ok(Statement::DropPublication { name, if_exists })
3082                    }
3083                    Token::Subscription => {
3084                        self.advance();
3085                        let if_exists = self.consume_if_exists();
3086                        let name = self.expect_ident_or_string()?;
3087                        Ok(Statement::DropSubscription { name, if_exists })
3088                    }
3089                    Token::Ident(s) | Token::QuotedIdent(s)
3090                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3091                    {
3092                        self.advance();
3093                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3094                        // login user IS a role in PG, and SPG's store holds
3095                        // both. `IF EXISTS` is accepted on either spelling.
3096                        let if_exists = self.consume_if_exists();
3097                        let name = self.expect_ident_or_string()?;
3098                        Ok(Statement::DropUser { name, if_exists })
3099                    }
3100                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3101                    // CREATE DATABASE has parsed since v7.14 and this did
3102                    // not, so `DROP DATABASE IF EXISTS x` — what every
3103                    // teardown script and pg_dumpall preamble opens with —
3104                    // came back as a syntax error, which IF EXISTS cannot
3105                    // soften. The name is carried so the engine can answer
3106                    // the way PG does; PG never lets this succeed on a
3107                    // single-database server, since the name is either
3108                    // unknown ("database … does not exist", or a notice
3109                    // under IF EXISTS) or the one you are connected to
3110                    // ("cannot drop the currently open database").
3111                    Token::Ident(s) | Token::QuotedIdent(s)
3112                        if s.eq_ignore_ascii_case("database") =>
3113                    {
3114                        self.advance();
3115                        let if_exists = self.consume_if_exists();
3116                        let name = self.expect_ident_or_string()?;
3117                        self.consume_until_statement_boundary();
3118                        Ok(Statement::DropDatabase { name, if_exists })
3119                    }
3120                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3121                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3122                        self.advance();
3123                        let if_exists = self.consume_if_exists();
3124                        let name = self.expect_ident_like()?;
3125                        // ON <table>
3126                        if !matches!(self.peek(), Token::On) {
3127                            return Err(self.err(alloc::format!(
3128                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3129                                self.peek()
3130                            )));
3131                        }
3132                        self.advance();
3133                        let table = self.expect_ident_like()?;
3134                        Ok(Statement::DropTrigger {
3135                            name,
3136                            table,
3137                            if_exists,
3138                        })
3139                    }
3140                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3141                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3142                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3143                        self.advance();
3144                        let if_exists = self.consume_if_exists();
3145                        let name = self.expect_ident_like()?;
3146                        if !matches!(self.peek(), Token::On) {
3147                            return Err(self.err(alloc::format!(
3148                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
3149                                self.peek()
3150                            )));
3151                        }
3152                        self.advance();
3153                        let table = self.expect_ident_like()?;
3154                        // Optional CASCADE / RESTRICT — accepted, no effect.
3155                        self.consume_until_statement_boundary();
3156                        Ok(Statement::DropRule {
3157                            name,
3158                            table,
3159                            if_exists,
3160                        })
3161                    }
3162                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3163                    // v7.12.4 ignores any optional arg-list (signature-
3164                    // based overload disambiguation lands in v7.12.5+).
3165                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3166                        self.advance();
3167                        let if_exists = self.consume_if_exists();
3168                        let name = self.expect_ident_like()?;
3169                        // v7.39 (read01 round 62) — the argument list identifies
3170                        // WHICH overload to drop, so it is captured, not
3171                        // discarded. `DROP FUNCTION f` (no list) is legal when
3172                        // the name is unambiguous; the engine enforces that.
3173                        let args = if matches!(self.peek(), Token::LParen) {
3174                            Some(self.parse_function_signature_types()?)
3175                        } else {
3176                            None
3177                        };
3178                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3179                        // trailer, which `DROP TABLE` and `DROP INDEX` have
3180                        // accepted since v7.14 and this one refused outright.
3181                        // pg_dump writes it, so refusing was a parse error in
3182                        // the middle of a restore. SPG drops the function
3183                        // either way — it tracks no dependents to cascade to —
3184                        // which is the same reading the other two give it.
3185                        self.consume_drop_behaviour();
3186                        Ok(Statement::DropFunction {
3187                            name,
3188                            args,
3189                            if_exists,
3190                        })
3191                    }
3192                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3193                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3194                    // emit DROP TABLE IF EXISTS at the head of every
3195                    // CREATE TABLE block so re-importing a dump
3196                    // overwrites prior state. SPG accepts and removes
3197                    // matching tables; CASCADE/RESTRICT trailers
3198                    // accepted silently.
3199                    Token::Table => self.parse_drop_table_after_keyword(),
3200                    // v7.14.0 — DROP INDEX [IF EXISTS] name
3201                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
3202                    // for partial-index renames and pgvector
3203                    // migrations. SPG removes the matching index;
3204                    // IF EXISTS makes the drop idempotent.
3205                    Token::Index => {
3206                        self.advance();
3207                        let if_exists_at = self.pos;
3208                        let if_exists = self.consume_if_exists();
3209                        let name = self.expect_ident_like()?;
3210                        // v7.39.7 — MySQL's own spelling, which SPG
3211                        // refused.
3212                        //
3213                        // `DROP INDEX i ON t` is how MySQL drops an
3214                        // index; its names live inside a table, so the
3215                        // statement names the table. Measured against
3216                        // MySQL 9.7.2: the form above works, and the
3217                        // bare `DROP INDEX i` PostgreSQL uses is a 1064
3218                        // there. SPG had it exactly backwards on the
3219                        // MySQL wire — the bare form accepted, MySQL's
3220                        // own a syntax error — so a migration that drops
3221                        // an index failed against the drop-in and not
3222                        // against the thing it replaces.
3223                        let table = if matches!(self.peek(), Token::On) {
3224                            self.advance();
3225                            Some(self.expect_ident_like()?)
3226                        } else {
3227                            None
3228                        };
3229                        if self.mysql_dialect {
3230                            // MySQL has no `IF EXISTS` here either:
3231                            // `DROP INDEX IF EXISTS i ON t` is a 1064.
3232                            if if_exists {
3233                                return Err(self.err_at(
3234                                    if_exists_at,
3235                                    "MySQL has no IF EXISTS on DROP INDEX".into(),
3236                                ));
3237                            }
3238                            if table.is_none() {
3239                                return Err(self.err("expected ON after the index name".into()));
3240                            }
3241                        }
3242                        if matches!(
3243                            self.peek(),
3244                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3245                                || s.eq_ignore_ascii_case("restrict")
3246                        ) {
3247                            self.advance();
3248                        }
3249                        Ok(Statement::DropIndex {
3250                            name,
3251                            if_exists,
3252                            table,
3253                        })
3254                    }
3255                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3256                    // [CASCADE|RESTRICT]. SPG is single-database;
3257                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3258                    // name [, name…] [CASCADE | RESTRICT]. Real
3259                    // unregister (was silent no-op pre-v7.17).
3260                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3261                        self.advance();
3262                        let if_exists = self.consume_if_exists();
3263                        let mut names = vec![self.expect_ident_like()?];
3264                        while matches!(self.peek(), Token::Comma) {
3265                            self.advance();
3266                            names.push(self.expect_ident_like()?);
3267                        }
3268                        if matches!(
3269                            self.peek(),
3270                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3271                                || s.eq_ignore_ascii_case("restrict")
3272                        ) {
3273                            self.advance();
3274                        }
3275                        Ok(Statement::DropSchema { names, if_exists })
3276                    }
3277                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3278                    // name [, name…] [CASCADE|RESTRICT].
3279                    Token::Ident(s) | Token::QuotedIdent(s)
3280                        if s.eq_ignore_ascii_case("type") =>
3281                    {
3282                        self.advance();
3283                        let if_exists = self.consume_if_exists();
3284                        let mut names = vec![self.expect_ident_like()?];
3285                        while matches!(self.peek(), Token::Comma) {
3286                            self.advance();
3287                            names.push(self.expect_ident_like()?);
3288                        }
3289                        if matches!(
3290                            self.peek(),
3291                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3292                                || s.eq_ignore_ascii_case("restrict")
3293                        ) {
3294                            self.advance();
3295                        }
3296                        Ok(Statement::DropType { names, if_exists })
3297                    }
3298                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3299                    // name [, name…] [CASCADE|RESTRICT].
3300                    Token::Ident(s) | Token::QuotedIdent(s)
3301                        if s.eq_ignore_ascii_case("domain") =>
3302                    {
3303                        self.advance();
3304                        let if_exists = self.consume_if_exists();
3305                        let mut names = vec![self.expect_ident_like()?];
3306                        while matches!(self.peek(), Token::Comma) {
3307                            self.advance();
3308                            names.push(self.expect_ident_like()?);
3309                        }
3310                        if matches!(
3311                            self.peek(),
3312                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3313                                || s.eq_ignore_ascii_case("restrict")
3314                        ) {
3315                            self.advance();
3316                        }
3317                        Ok(Statement::DropDomain { names, if_exists })
3318                    }
3319                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3320                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3321                    Token::Ident(s) | Token::QuotedIdent(s)
3322                        if s.eq_ignore_ascii_case("materialized") =>
3323                    {
3324                        self.advance();
3325                        let nxt = self.peek().clone();
3326                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3327                        {
3328                            return Err(self.err(alloc::format!(
3329                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3330                            )));
3331                        }
3332                        self.advance();
3333                        let if_exists = self.consume_if_exists();
3334                        let mut names = vec![self.expect_ident_like()?];
3335                        while matches!(self.peek(), Token::Comma) {
3336                            self.advance();
3337                            names.push(self.expect_ident_like()?);
3338                        }
3339                        if matches!(
3340                            self.peek(),
3341                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3342                                || s.eq_ignore_ascii_case("restrict")
3343                        ) {
3344                            self.advance();
3345                        }
3346                        Ok(Statement::DropMaterializedView { names, if_exists })
3347                    }
3348                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3349                    // name [, name…] [CASCADE|RESTRICT].
3350                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3351                        self.advance();
3352                        let if_exists = self.consume_if_exists();
3353                        let mut names = vec![self.expect_ident_like()?];
3354                        while matches!(self.peek(), Token::Comma) {
3355                            self.advance();
3356                            names.push(self.expect_ident_like()?);
3357                        }
3358                        if matches!(
3359                            self.peek(),
3360                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3361                                || s.eq_ignore_ascii_case("restrict")
3362                        ) {
3363                            self.advance();
3364                        }
3365                        Ok(Statement::DropView { names, if_exists })
3366                    }
3367                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3368                    // [CASCADE|RESTRICT]. Real removal from catalog
3369                    // (was a silent no-op pre-v7.17).
3370                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3371                        self.advance();
3372                        let if_exists = self.consume_if_exists();
3373                        let mut names = vec![self.expect_ident_like()?];
3374                        while matches!(self.peek(), Token::Comma) {
3375                            self.advance();
3376                            names.push(self.expect_ident_like()?);
3377                        }
3378                        if matches!(
3379                            self.peek(),
3380                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3381                                || s.eq_ignore_ascii_case("restrict")
3382                        ) {
3383                            self.advance();
3384                        }
3385                        Ok(Statement::DropSequence { names, if_exists })
3386                    }
3387                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3388                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3389                        self.advance();
3390                        self.parse_drop_policy_after_keyword()
3391                    }
3392                    // v7.37.17 (17.6 siblings) — DROP <target> for
3393                    // targets SPG doesn't natively track. pg_dump
3394                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3395                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3396                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3397                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3398                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3399                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3400                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3401                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3402                    // etc. — accept + Empty-return so pg_dump tails
3403                    // load through. Materialized-view drop dispatches
3404                    // to the existing DropTable path when the token
3405                    // is Materialized-View-shaped (elsewhere in
3406                    // this parser).
3407                    Token::Ident(s) | Token::QuotedIdent(s)
3408                        if s.eq_ignore_ascii_case("text")
3409                            // The DROP dispatch matches on PEEK — `text` is
3410                            // not yet consumed, so SEARCH/CONFIGURATION sit
3411                            // at pos+1/pos+2 (the round-695 trap's mirror).
3412                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3413                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3414                    {
3415                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3416                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3417                        // stay in the noise arm below.
3418                        self.advance(); // TEXT
3419                        self.advance(); // SEARCH
3420                        self.advance(); // CONFIGURATION
3421                        let if_exists = self.consume_if_exists();
3422                        let names = self.take_comma_separated_names();
3423                        self.consume_until_statement_boundary();
3424                        if if_exists {
3425                            return Ok(Statement::Empty);
3426                        }
3427                        Ok(Statement::ValidateOnly {
3428                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3429                            names,
3430                        })
3431                    }
3432                    Token::Ident(s) | Token::QuotedIdent(s)
3433                        if matches!(
3434                            s.to_ascii_lowercase().as_str(),
3435                            "type"
3436                                | "domain"
3437                                | "operator"
3438                                | "cast"
3439                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3440                                // TEMPLATE (CONFIGURATION intercepted above).
3441                                | "text"
3442                                | "materialized"
3443                                | "large"
3444                                | "role"
3445                                | "access"
3446                                | "procedure"
3447                                | "routine"
3448                        ) =>
3449                    {
3450                        self.consume_until_statement_boundary();
3451                        Ok(Statement::Empty)
3452                    }
3453                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3454                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3455                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3456                    // foreign-data warning family (round 706) so a
3457                    // CREATE→DROP sequence in a dump stays consistent.
3458                    Token::Ident(s) | Token::QuotedIdent(s)
3459                        if s.eq_ignore_ascii_case("server")
3460                            || s.eq_ignore_ascii_case("foreign") =>
3461                    {
3462                        self.advance();
3463                        self.consume_until_statement_boundary();
3464                        Ok(Statement::ValidateOnly {
3465                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3466                            names: Vec::new(),
3467                        })
3468                    }
3469                    Token::Ident(s) | Token::QuotedIdent(s)
3470                        if s.eq_ignore_ascii_case("collation")
3471                            || s.eq_ignore_ascii_case("tablespace") =>
3472                    {
3473                        let kind = if s.eq_ignore_ascii_case("collation") {
3474                            crate::ast::ValidateOnlyKind::CollationName
3475                        } else {
3476                            crate::ast::ValidateOnlyKind::TablespaceName
3477                        };
3478                        self.advance();
3479                        let if_exists = self.consume_if_exists();
3480                        let names = self.take_comma_separated_names();
3481                        self.consume_until_statement_boundary();
3482                        if if_exists {
3483                            return Ok(Statement::Empty);
3484                        }
3485                        Ok(Statement::ValidateOnly { kind, names })
3486                    }
3487                    Token::Ident(s) | Token::QuotedIdent(s)
3488                        if s.eq_ignore_ascii_case("event") =>
3489                    {
3490                        self.advance();
3491                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3492                        {
3493                            self.advance();
3494                        }
3495                        let if_exists = self.consume_if_exists();
3496                        let names = self.take_comma_separated_names();
3497                        self.consume_until_statement_boundary();
3498                        if if_exists {
3499                            return Ok(Statement::Empty);
3500                        }
3501                        Ok(Statement::ValidateOnly {
3502                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3503                            names,
3504                        })
3505                    }
3506                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3507                    // leave the noise list; see the ValidateOnly kinds.
3508                    Token::Ident(s) | Token::QuotedIdent(s)
3509                        if s.eq_ignore_ascii_case("conversion")
3510                            || s.eq_ignore_ascii_case("language")
3511                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3512                            // FIRST — the first draft looked for it after.
3513                            || s.eq_ignore_ascii_case("procedural") =>
3514                    {
3515                        let kind = if s.eq_ignore_ascii_case("conversion") {
3516                            crate::ast::ValidateOnlyKind::ConversionName
3517                        } else {
3518                            crate::ast::ValidateOnlyKind::LanguageName
3519                        };
3520                        self.advance();
3521                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3522                        {
3523                            self.advance();
3524                        }
3525                        let if_exists = self.consume_if_exists();
3526                        let names = self.take_comma_separated_names();
3527                        self.consume_until_statement_boundary();
3528                        if if_exists {
3529                            return Ok(Statement::Empty);
3530                        }
3531                        Ok(Statement::ValidateOnly { kind, names })
3532                    }
3533                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3534                    // name(argtypes)[, …]`. Parsed for real so the engine
3535                    // can answer as PG does; see Statement::DropAggregate.
3536                    Token::Ident(s) | Token::QuotedIdent(s)
3537                        if s.eq_ignore_ascii_case("aggregate") =>
3538                    {
3539                        self.advance();
3540                        let if_exists = self.consume_if_exists();
3541                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3542                        loop {
3543                            let name = self.expect_ident_like()?;
3544                            if !matches!(self.peek(), Token::LParen) {
3545                                return Err(self.err(alloc::format!(
3546                                    "expected argument list after DROP AGGREGATE {name}"
3547                                )));
3548                            }
3549                            self.advance();
3550                            let mut args: Vec<String> = Vec::new();
3551                            let mut star = false;
3552                            loop {
3553                                match self.peek().clone() {
3554                                    Token::RParen => {
3555                                        self.advance();
3556                                        break;
3557                                    }
3558                                    Token::Star => {
3559                                        self.advance();
3560                                        star = true;
3561                                    }
3562                                    Token::Comma => {
3563                                        self.advance();
3564                                    }
3565                                    _ => {
3566                                        // A type name may be multi-token
3567                                        // (`double precision`); glue idents
3568                                        // until , or ).
3569                                        let mut t = self.expect_ident_like()?;
3570                                        while let Token::Ident(nx) = self.peek() {
3571                                            let nx = nx.clone();
3572                                            self.advance();
3573                                            t.push(' ');
3574                                            t.push_str(&nx);
3575                                        }
3576                                        args.push(t);
3577                                    }
3578                                }
3579                            }
3580                            items.push((name, if star { None } else { Some(args) }));
3581                            if matches!(self.peek(), Token::Comma) {
3582                                self.advance();
3583                            } else {
3584                                break;
3585                            }
3586                        }
3587                        self.consume_until_statement_boundary();
3588                        Ok(Statement::DropAggregate { if_exists, items })
3589                    }
3590                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3591                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3592                    // installed; `IF EXISTS` is the spelling that says do
3593                    // not, and it keeps the no-op.
3594                    Token::Ident(s) | Token::QuotedIdent(s)
3595                        if s.eq_ignore_ascii_case("extension") =>
3596                    {
3597                        self.advance();
3598                        let if_exists = self.consume_if_exists();
3599                        let names = self.take_comma_separated_names();
3600                        self.consume_until_statement_boundary();
3601                        if if_exists {
3602                            return Ok(Statement::Empty);
3603                        }
3604                        Ok(Statement::ValidateOnly {
3605                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3606                            names,
3607                        })
3608                    }
3609                    Token::Ident(s) | Token::QuotedIdent(s)
3610                        if s.eq_ignore_ascii_case("statistics") =>
3611                    {
3612                        self.parse_drop_statistics_after_drop()
3613                    }
3614                    other => Err(self.err(format!(
3615                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3616                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3617                    ))),
3618                }
3619            }
3620            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3621            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3622            // and accepted before the view name. SPG materialised
3623            // views re-evaluate on read (always-fresh semantics), so
3624            // the CONCURRENTLY-vs-serial distinction has no runtime
3625            // effect — the refresh body does not block readers either
3626            // way. Same accept-and-no-op pattern as DETACH PARTITION
3627            // CONCURRENTLY (16.5).
3628            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3629                self.advance();
3630                let nxt = self.peek().clone();
3631                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3632                {
3633                    return Err(self.err(alloc::format!(
3634                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3635                    )));
3636                }
3637                self.advance();
3638                let nxt2 = self.peek().clone();
3639                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3640                {
3641                    return Err(self.err(alloc::format!(
3642                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3643                    )));
3644                }
3645                self.advance();
3646                // Optional CONCURRENTLY noise word — consumed without
3647                // changing semantics.
3648                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3649                {
3650                    self.advance();
3651                }
3652                let name = self.expect_ident_like()?;
3653                let with_data = self.parse_optional_with_data(true)?;
3654                Ok(Statement::RefreshMaterializedView { name, with_data })
3655            }
3656            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3657                self.advance();
3658                self.parse_update_after_keyword()
3659            }
3660            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3661            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3662            // [CASCADE | RESTRICT]. Clears every row from each named
3663            // table. Parses at the top level; the engine dispatcher
3664            // walks Statement::Truncate.
3665            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3666                self.advance();
3667                // Optional TABLE noise word — PG accepts both the reserved
3668                // token and the bare identifier spelling.
3669                if matches!(self.peek(), Token::Table)
3670                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3671                {
3672                    self.advance();
3673                }
3674                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3675                // not absorbed. The lookahead keeps a table genuinely
3676                // called `only` working: the keyword is a keyword only
3677                // when a name follows it.
3678                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3679                    if s.eq_ignore_ascii_case("only"))
3680                    && matches!(
3681                        self.tokens.get(self.pos + 1),
3682                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3683                    );
3684                if only {
3685                    self.advance();
3686                }
3687                // Table names (comma-separated).
3688                let mut tables = Vec::new();
3689                loop {
3690                    tables.push(self.expect_ident_like()?);
3691                    if matches!(self.peek(), Token::Comma) {
3692                        self.advance();
3693                        continue;
3694                    }
3695                    break;
3696                }
3697                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3698                let mut restart_identity = false;
3699                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3700                {
3701                    self.advance();
3702                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3703                    {
3704                        self.advance();
3705                        restart_identity = true;
3706                    }
3707                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3708                {
3709                    self.advance();
3710                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3711                    {
3712                        self.advance();
3713                    }
3714                }
3715                // Optional CASCADE / RESTRICT.
3716                let mut cascade = false;
3717                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3718                {
3719                    self.advance();
3720                    cascade = true;
3721                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3722                {
3723                    self.advance();
3724                }
3725                Ok(Statement::Truncate {
3726                    tables,
3727                    restart_identity,
3728                    cascade,
3729                    only,
3730                })
3731            }
3732            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3733            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3734            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3735            // rows change so the index tree is always up-to-date;
3736            // REINDEX is a strict no-op. Accept the whole statement
3737            // shape to boundary for pg_dump round-trip compatibility.
3738            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3739                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3740                // index bloat to rebuild, so the work stays a no-op, but PG
3741                // validates what it was pointed at and this swallowed the
3742                // name at parse time — `REINDEX TABLE typo` reported
3743                // success. Measured on PG18: INDEX / TABLE name a relation,
3744                // SCHEMA a schema, SYSTEM nothing.
3745                self.advance();
3746                self.parse_reindex_tail()
3747            }
3748            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3749            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3750            // SPG has no MVCC bloat today (Phase D visibility map
3751            // queues with v7.38); the freezer collapses hot-tier
3752            // rows into cold segments automatically. VACUUM is a
3753            // no-op — pg_dump maintenance scripts and Discourse's
3754            // periodic-maintenance path both emit it.
3755            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3756            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3757            // actual bloat, so the pre-MVCC accept-and-ignore posture
3758            // became a silent no-op on a customer's manual reclaim.
3759            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3760            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3761            // ANALYZE is captured, the optional table name is captured.
3762            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3763                self.advance();
3764                // Parenthesised option list: absorb it.
3765                if matches!(self.peek(), Token::LParen) {
3766                    let mut depth = 0usize;
3767                    loop {
3768                        match self.advance() {
3769                            Token::LParen => depth += 1,
3770                            Token::RParen => {
3771                                depth -= 1;
3772                                if depth == 0 {
3773                                    break;
3774                                }
3775                            }
3776                            Token::Eof => break,
3777                            _ => {}
3778                        }
3779                    }
3780                }
3781                let mut analyze = false;
3782                let mut table: Option<String> = None;
3783                loop {
3784                    match self.peek() {
3785                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3786                        // an identifier, so the loop below broke out on it and
3787                        // dropped the table name: `VACUUM FULL nosuch` was
3788                        // accepted where `VACUUM nosuch` was refused.
3789                        Token::Full => {
3790                            self.advance();
3791                        }
3792                        Token::Ident(w) | Token::QuotedIdent(w) => {
3793                            let wl = w.to_ascii_lowercase();
3794                            match wl.as_str() {
3795                                "full" | "freeze" | "verbose" => {
3796                                    self.advance();
3797                                }
3798                                "analyze" | "analyse" => {
3799                                    analyze = true;
3800                                    self.advance();
3801                                }
3802                                _ => {
3803                                    table = Some(self.expect_ident_like()?);
3804                                    break;
3805                                }
3806                            }
3807                        }
3808                        _ => break,
3809                    }
3810                }
3811                // Optional trailing column list / anything else to the
3812                // statement boundary (PG accepts per-column ANALYZE).
3813                self.consume_until_statement_boundary();
3814                Ok(Statement::Vacuum { table, analyze })
3815            }
3816            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3817            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3818            // <index>. PG stores rows in physical order matching
3819            // an index; SPG's hot-tier is append-only + cold-tier
3820            // is segment-frozen, so clustering has no persistent
3821            // effect. Accept-and-no-op for pg_dump compat.
3822            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3823                // v7.39 (round 535) — same as REINDEX above: the relation is
3824                // carried so the engine can refuse one that does not exist.
3825                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3826                self.advance();
3827                self.parse_cluster_tail()
3828            }
3829            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3830            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3831            // optional string payload; UNLISTEN takes a channel or `*`.
3832            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3833                self.advance();
3834                let ch = match self.advance() {
3835                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3836                    other => {
3837                        return Err(self.err(format!(
3838                            "expected channel name after LISTEN, got {other:?}"
3839                        )));
3840                    }
3841                };
3842                Ok(Statement::Listen(ch))
3843            }
3844            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3845                self.advance();
3846                let channel = match self.advance() {
3847                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3848                    other => {
3849                        return Err(self.err(format!(
3850                            "expected channel name after NOTIFY, got {other:?}"
3851                        )));
3852                    }
3853                };
3854                let payload = if matches!(self.peek(), Token::Comma) {
3855                    self.advance();
3856                    match self.advance() {
3857                        Token::String(p) => Some(p),
3858                        other => {
3859                            return Err(self.err(format!(
3860                                "expected string payload after NOTIFY <channel>, got {other:?}"
3861                            )));
3862                        }
3863                    }
3864                } else {
3865                    None
3866                };
3867                Ok(Statement::Notify { channel, payload })
3868            }
3869            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3870                self.advance();
3871                match self.advance() {
3872                    Token::Star => Ok(Statement::Unlisten(None)),
3873                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3874                    other => Err(self.err(format!(
3875                        "expected channel name or * after UNLISTEN, got {other:?}"
3876                    ))),
3877                }
3878            }
3879            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3880            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3881            // process-wide write lock today; explicit LOCK has no
3882            // effect. Accept-and-no-op for pg_dump / migration
3883            // compat.
3884            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3885                self.advance();
3886                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3887                // engine holds a process-wide write lock), but the TABLE
3888                // NAME is now carried out so the engine can refuse one that
3889                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3890                // READ|WRITE` is a different statement with the same first
3891                // word; it keeps the old no-op, because a MySQL dump's
3892                // bracket names tables it is about to create.
3893                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3894                    if k.eq_ignore_ascii_case("tables"));
3895                if mysql_tables {
3896                    self.consume_until_statement_boundary();
3897                    return Ok(Statement::Empty);
3898                }
3899                if matches!(self.peek(), Token::Table) {
3900                    self.advance();
3901                }
3902                let names = self.take_comma_separated_names();
3903                self.consume_until_statement_boundary();
3904                Ok(Statement::ValidateOnly {
3905                    kind: crate::ast::ValidateOnlyKind::LockTable,
3906                    names,
3907                })
3908            }
3909            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3910            // durability marker + snapshot in PG. SPG has WAL
3911            // checkpointing on a byte / time schedule (v7.37.10
3912            // 60s / 4 MiB defaults). The bare statement parses to
3913            // `Statement::Empty` here (the no_std engine owns no
3914            // WAL / snapshot); v7.37 Epic Du wires the HOST
3915            // (embedded `Database::execute_buffered`, via
3916            // `sql_is_checkpoint`) to force an immediate synchronous
3917            // checkpoint through `Database::checkpoint` — a real
3918            // durability barrier, matching PG.
3919            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3920                self.advance();
3921                self.consume_until_statement_boundary();
3922                Ok(Statement::Empty)
3923            }
3924            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3925                self.advance();
3926                self.parse_delete_after_keyword()
3927            }
3928            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3929            // ALTER is not a reserved keyword in the lexer — handled
3930            // as a bare ident here.
3931            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3932                self.advance();
3933                self.parse_alter_after_keyword()
3934            }
3935            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3936            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3937            // additions needed.
3938            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3939                self.advance();
3940                self.parse_wait_after_keyword()
3941            }
3942            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3943            // Bare ANALYZE → analyse every user table; ANALYZE
3944            // <name> → re-stats one. The argument is an optional
3945            // ident (or quoted ident); anything else is a parse
3946            // error.
3947            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3948            // `WHERE` filter (carved out per V6_7_DESIGN.md
3949            // STABILITY). Lex order: identifier "compact" → "cold"
3950            // → "segments". Anything else after `COMPACT` is a
3951            // parse error.
3952            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3953                self.advance();
3954                let next = self.peek().clone();
3955                let cold = match next {
3956                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3957                    _ => {
3958                        return Err(
3959                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3960                        );
3961                    }
3962                };
3963                if !cold.eq_ignore_ascii_case("cold") {
3964                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3965                }
3966                self.advance();
3967                let next = self.peek().clone();
3968                let segments = match next {
3969                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3970                    _ => {
3971                        return Err(self.err(format!(
3972                            "expected SEGMENTS after COMPACT COLD, got {:?}",
3973                            self.peek()
3974                        )));
3975                    }
3976                };
3977                if !segments.eq_ignore_ascii_case("segments") {
3978                    return Err(self.err(format!(
3979                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
3980                    )));
3981                }
3982                self.advance();
3983                Ok(Statement::CompactColdSegments)
3984            }
3985            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
3986            // Parsed as a case-insensitive identifier since MERGE
3987            // isn't a reserved lexer keyword (collides with the
3988            // mysqldump `ALGORITHM = MERGE` view clause if it
3989            // were); the inner parser drives the rest of the
3990            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
3991            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
3992                self.advance();
3993                self.parse_merge_after_keyword()
3994            }
3995            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
3996                self.advance();
3997                let target = match self.peek() {
3998                    Token::Eof | Token::Semicolon => None,
3999                    Token::Ident(_) | Token::QuotedIdent(_) => {
4000                        Some(self.expect_ident_like()?)
4001                    }
4002                    other => {
4003                        return Err(self.err(format!(
4004                            "expected table name or end of statement after ANALYZE, got {other:?}"
4005                        )));
4006                    }
4007                };
4008                // v7.39 (round 776, F31 J7) — the per-column form
4009                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
4010                // here while the VACUUM arm already consumed it; SPG
4011                // analyzes whole tables, so the list parses and is
4012                // accepted like the VACUUM path's.
4013                if target.is_some() && matches!(self.peek(), Token::LParen) {
4014                    self.advance();
4015                    loop {
4016                        let _ = self.expect_ident_like()?;
4017                        match self.peek() {
4018                            Token::Comma => {
4019                                self.advance();
4020                            }
4021                            Token::RParen => {
4022                                self.advance();
4023                                break;
4024                            }
4025                            other => {
4026                                return Err(self.err(format!(
4027                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
4028                                )));
4029                            }
4030                        }
4031                    }
4032                }
4033                Ok(Statement::Analyze(target))
4034            }
4035            // v7.12.1 — `SET <name> [TO|=] <value>`. The
4036            // `default_text_search_config` parameter is consumed
4037            // by the FTS function dispatcher; other parameter
4038            // names are recorded but treated as a no-op so PG
4039            // dump output loads.
4040            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4041                self.advance();
4042                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4043                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4044                // …` which the SessionVar path handles). `LOCAL` is the only
4045                // one that changes semantics — it scopes the change to the
4046                // current transaction — so capture it; SESSION / GLOBAL are
4047                // accepted and treated as the default session scope.
4048                let mut set_local = false;
4049                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4050                    let q = s.to_ascii_lowercase();
4051                    if q == "local" || q == "session" || q == "global" {
4052                        set_local = q == "local";
4053                        self.advance();
4054                    }
4055                }
4056                // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4057                // { DEFAULT | <role> }`. pg_dump's ACL section switches
4058                // to the object owner with it. SPG maps it onto the
4059                // session-role machinery (recorded delta RD-10: PG moves
4060                // session_user too; SPG moves the effective role).
4061                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4062                    if s.eq_ignore_ascii_case("authorization"))
4063                {
4064                    self.advance(); // AUTHORIZATION
4065                    let role = match self.peek().clone() {
4066                        Token::Default => {
4067                            self.advance();
4068                            None
4069                        }
4070                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4071                            self.advance();
4072                            Some(s)
4073                        }
4074                        _ => None,
4075                    };
4076                    return Ok(Statement::SetRole(role));
4077                }
4078                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4079                // <collation>]` — change the connection client
4080                // charset. SPG stores UTF-8 always and orders
4081                // bytewise; accept as a no-op.
4082                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4083                {
4084                    self.advance();
4085                    // v7.39 — this used to parse the clause and throw it
4086                    // away ("SPG stores UTF-8 always and orders
4087                    // bytewise; accept as a no-op"). That sentence
4088                    // stopped being true when collations arrived, and
4089                    // once `collation_connection` began driving literal
4090                    // comparison, dropping the COLLATE clause became a
4091                    // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4092                    // utf8mb4_general_ci` reported back
4093                    // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4094                    //
4095                    // The charset name is emitted as `names` and the
4096                    // ENGINE expands it, because which collation a
4097                    // charset defaults to is MySQL semantics and belongs
4098                    // beside the rest of them, not in the parser.
4099                    let mut pairs = alloc::vec::Vec::new();
4100                    if matches!(
4101                        self.peek(),
4102                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4103                    ) {
4104                        let charset = match self.advance() {
4105                            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4106                            _ => unreachable!("peeked an ident-or-string"),
4107                        };
4108                        pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4109                    }
4110                    // Optional `COLLATE <name>` — emitted AFTER `names`
4111                    // so it overrides the charset's default, which is
4112                    // what MySQL does.
4113                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4114                    {
4115                        self.advance();
4116                        if matches!(
4117                            self.peek(),
4118                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4119                        ) {
4120                            let coll = match self.advance() {
4121                                Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4122                                _ => unreachable!("peeked an ident-or-string"),
4123                            };
4124                            pairs.push((
4125                                String::from("collation_connection"),
4126                                crate::ast::SetValue::Ident(coll),
4127                            ));
4128                        }
4129                    }
4130                    if pairs.is_empty() {
4131                        return Ok(Statement::Empty);
4132                    }
4133                    return Ok(Statement::SetParameterList(pairs));
4134                }
4135                // v7.37.17 (17.6 sibling) — PG `SET ROLE
4136                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4137                // uses this to switch to the object owner before
4138                // recreating tables. SPG has no role system so this
4139                // is a no-op.
4140                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4141                {
4142                    self.advance(); // ROLE
4143                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4144                    // reset to the login identity; a name / string sets the
4145                    // effective role that drives current_user + RLS.
4146                    let role = match self.peek().clone() {
4147                        Token::Default => {
4148                            self.advance();
4149                            None
4150                        }
4151                        Token::Ident(s) | Token::QuotedIdent(s)
4152                            if s.eq_ignore_ascii_case("none") =>
4153                        {
4154                            self.advance();
4155                            None
4156                        }
4157                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4158                            self.advance();
4159                            Some(s)
4160                        }
4161                        _ => None,
4162                    };
4163                    return Ok(Statement::SetRole(role));
4164                }
4165                // v7.37.17 (17.6 sibling) — PG `SET SESSION
4166                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4167                // ISO SQL surface). pg_dump prepends this to fix
4168                // the isolation level for the restore session. SPG
4169                // defaults to READ COMMITTED and doesn't yet honor
4170                // session-set isolation across statements — accept
4171                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4172                // per-tx form is handled elsewhere.
4173                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4174                {
4175                    self.advance(); // CHARACTERISTICS
4176                    if matches!(self.peek(), Token::As) {
4177                        self.advance();
4178                    }
4179                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4180                        self.advance();
4181                    }
4182                    // v7.39 — no longer a no-op. The note above said SPG
4183                    // "doesn't yet honor session-set isolation across
4184                    // statements"; it does now, through
4185                    // `default_transaction_isolation`, and measured on
4186                    // PG 18.6 this statement is exactly a way to set it:
4187                    //
4188                    //   SET SESSION CHARACTERISTICS AS TRANSACTION
4189                    //       ISOLATION LEVEL REPEATABLE READ;
4190                    //   current_setting('default_transaction_isolation')
4191                    //       -> repeatable read
4192                    //
4193                    // pg_dump prepends this to fix the level for a
4194                    // restore session, so accepting it and doing nothing
4195                    // meant the restore ran at a level nobody chose.
4196                    //
4197                    // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4198                    // still consumed and dropped. `default_transaction_read_only`
4199                    // exists in the GUC inventory but nothing enforces it,
4200                    // and setting a value no code honours is the very
4201                    // defect this version is about — a session told it
4202                    // holds a guarantee it does not.
4203                    let modes = self.parse_isolation_level_clauses()?;
4204                    self.consume_until_statement_boundary();
4205                    let mut pairs: alloc::vec::Vec<(
4206                        alloc::string::String,
4207                        crate::ast::SetValue,
4208                    )> = alloc::vec::Vec::new();
4209                    if let Some(level) = modes.isolation {
4210                        pairs.push((
4211                            alloc::string::String::from("default_transaction_isolation"),
4212                            crate::ast::SetValue::String(alloc::string::String::from(
4213                                level.as_pg_str(),
4214                            )),
4215                        ));
4216                    }
4217                    if let Some(ro) = modes.read_only {
4218                        pairs.push((
4219                            alloc::string::String::from("default_transaction_read_only"),
4220                            crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4221                                "on"
4222                            } else {
4223                                "off"
4224                            })),
4225                        ));
4226                    }
4227                    return Ok(if pairs.is_empty() {
4228                        Statement::Empty
4229                    } else {
4230                        Statement::SetParameterList(pairs)
4231                    });
4232                }
4233                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4234                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4235                // pg_dump emits this to control the deferrability of
4236                // FK / UNIQUE constraints across a bulk restore. SPG
4237                // has no deferrable-constraint machinery today; the
4238                // FK checker is strict-immediate. Accept-and-no-op
4239                // for pg_dump round-trip compatibility.
4240                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4241                {
4242                    self.advance(); // CONSTRAINTS
4243                    // v7.39 (round 288) — no longer a no-op: the trailing
4244                    // DEFERRED / IMMEDIATE sets the transaction's timing.
4245                    // v7.39 (round 308, V29) — and the names are kept.
4246                    // They used to be skipped over on the way to the
4247                    // DEFERRED keyword, so a named form silently behaved
4248                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4249                    // every deferrable constraint in the transaction.
4250                    let mut names: alloc::vec::Vec<alloc::string::String> =
4251                        alloc::vec::Vec::new();
4252                    if matches!(self.peek(), Token::All) {
4253                        self.advance();
4254                    } else {
4255                        loop {
4256                            let mut n = self.expect_ident_like()?;
4257                            // A schema-qualified name (`public.fk_a`)
4258                            // identifies the same constraint; PG resolves
4259                            // it by the trailing segment.
4260                            while matches!(self.peek(), Token::Dot) {
4261                                self.advance();
4262                                n = self.expect_ident_like()?;
4263                            }
4264                            names.push(n);
4265                            if matches!(self.peek(), Token::Comma) {
4266                                self.advance();
4267                            } else {
4268                                break;
4269                            }
4270                        }
4271                    }
4272                    let deferred = match self.peek() {
4273                        Token::Ident(s) | Token::QuotedIdent(s)
4274                            if s.eq_ignore_ascii_case("deferred") =>
4275                        {
4276                            true
4277                        }
4278                        Token::Ident(s) | Token::QuotedIdent(s)
4279                            if s.eq_ignore_ascii_case("immediate") =>
4280                        {
4281                            false
4282                        }
4283                        other => {
4284                            return Err(self.err(alloc::format!(
4285                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4286                            )));
4287                        }
4288                    };
4289                    self.advance();
4290                    return Ok(Statement::SetConstraints { names, deferred });
4291                }
4292                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4293                // { DEFAULT | '<role>' | <ident> }` (mailrs
4294                // round-10 A.1). pg_dump preamble emits the
4295                // `DEFAULT` form to reset session authorization.
4296                //
4297                // v7.39 (round 697) — this said "SPG has no role system so
4298                // this is a strict no-op". SPG has had one since round 58;
4299                // the comment outlived it, and with it the reason a name
4300                // that is not a role was accepted here. It still switches
4301                // no authorization — what it does now is refuse a role
4302                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4303                // AUTHORIZATION` (handled by the RESET parser
4304                // elsewhere). Reference:
4305                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4306                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4307                {
4308                    self.advance(); // AUTHORIZATION
4309                    match self.peek().clone() {
4310                        Token::Default => {
4311                            self.advance();
4312                        }
4313                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4314                            self.advance();
4315                            return Ok(Statement::ValidateOnly {
4316                                kind: crate::ast::ValidateOnlyKind::RoleName,
4317                                names: alloc::vec![r],
4318                            });
4319                        }
4320                        other => {
4321                            return Err(self.err(alloc::format!(
4322                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4323                            )));
4324                        }
4325                    }
4326                    return Ok(Statement::Empty);
4327                }
4328                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4329                // ISOLATION LEVEL { READ COMMITTED | READ
4330                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4331                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4332                // PG-standard surface. v7.37.8 accepts the syntax
4333                // and tracks the selected level on
4334                // `Engine::current_isolation_level()`; the actual
4335                // MVCC / SSI semantics implementation lands in
4336                // the 轴 4 isolation framework (separate train).
4337                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4338                // internally; SPG behaves the same (effectively
4339                // READ COMMITTED at every level today).
4340                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4341                {
4342                    self.advance(); // TRANSACTION
4343                    let modes = self.parse_isolation_level_clauses()?;
4344                    return Ok(Statement::SetTransaction { modes });
4345                }
4346                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4347                // alias — same accept-as-no-op as SET NAMES.
4348                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4349                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4350                {
4351                    self.advance(); // CHARACTER
4352                    self.advance(); // SET
4353                    if matches!(
4354                        self.peek(),
4355                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4356                    ) {
4357                        self.advance();
4358                    }
4359                    return Ok(Statement::Empty);
4360                }
4361                // v7.39 (GUC) — PG spells the timezone GUC as two
4362                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4363                // where <value> is a string/ident or the LOCAL /
4364                // DEFAULT keyword (both mean "back to the default").
4365                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4366                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4367                {
4368                    self.advance(); // TIME
4369                    self.advance(); // ZONE
4370                    let value = match self.peek().clone() {
4371                        Token::Ident(s)
4372                            if s.eq_ignore_ascii_case("local")
4373                                || s.eq_ignore_ascii_case("default") =>
4374                        {
4375                            self.advance();
4376                            crate::ast::SetValue::Default
4377                        }
4378                        Token::Default => {
4379                            self.advance();
4380                            crate::ast::SetValue::Default
4381                        }
4382                        _ => self.parse_set_value()?,
4383                    };
4384                    return Ok(Statement::SetParameter {
4385                        name: "timezone".into(),
4386                        value,
4387                        local: set_local,
4388                    });
4389                }
4390                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4391                // MySQL USER-variable assignment: its own per-session
4392                // namespace, an arbitrary expression on the right, and `:=`
4393                // as a second spelling of `=`. It used to fall into the
4394                // session-PARAMETER list below, whose values are literals and
4395                // whose store nothing reads back under a `@` name — so the
4396                // assignment reported success and vanished.
4397                //
4398                // A `@@`-prefixed LHS is a real engine setting and keeps the
4399                // old path.
4400                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4401                    return self.parse_set_user_vars();
4402                }
4403                // v7.14.0 — multi-assignment form
4404                // `SET a = 1, b = 2, …`. Single-assignment is the
4405                // 1-element case. Each LHS may be a regular ident
4406                // or a SessionVar (`@VAR` / `@@VAR`).
4407                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4408                loop {
4409                    let lhs = match self.peek().clone() {
4410                        Token::SessionVar(s) => {
4411                            self.advance();
4412                            s
4413                        }
4414                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4415                        other => {
4416                            return Err(self.err(format!(
4417                                "expected parameter name after SET, got {other:?}"
4418                            )));
4419                        }
4420                    };
4421                    // Accept either `=` or the bare `TO` keyword.
4422                    match self.peek() {
4423                        Token::Eq => {
4424                            self.advance();
4425                        }
4426                        Token::To => {
4427                            self.advance();
4428                        }
4429                        other => {
4430                            return Err(self.err(format!(
4431                                "expected `=` or TO after SET {lhs}, got {other:?}"
4432                            )));
4433                        }
4434                    }
4435                    let mut value = self.parse_set_value()?;
4436                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4437                    // `, name TO` continues a MySQL-style multi-assign,
4438                    // anything else is a PG list VALUE
4439                    // (`SET search_path = myschema, public`) folded into
4440                    // one comma-joined string.
4441                    while matches!(self.peek(), Token::Comma) {
4442                        let is_assign = matches!(
4443                            self.tokens.get(self.pos + 1),
4444                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4445                        ) && matches!(
4446                            self.tokens.get(self.pos + 2),
4447                            Some(Token::Eq | Token::To)
4448                        );
4449                        if is_assign {
4450                            break;
4451                        }
4452                        self.advance(); // comma
4453                        let next = self.parse_set_value()?;
4454                        let joined = alloc::format!(
4455                            "{}, {}",
4456                            set_value_text(&value),
4457                            set_value_text(&next)
4458                        );
4459                        value = crate::ast::SetValue::String(joined);
4460                    }
4461                    pairs.push((lhs, value));
4462                    if matches!(self.peek(), Token::Comma) {
4463                        self.advance();
4464                        continue;
4465                    }
4466                    break;
4467                }
4468                if pairs.len() == 1 {
4469                    let (name, value) = pairs.into_iter().next().unwrap();
4470                    Ok(Statement::SetParameter {
4471                        name,
4472                        value,
4473                        local: set_local,
4474                    })
4475                } else {
4476                    Ok(Statement::SetParameterList(pairs))
4477                }
4478            }
4479            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4480            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4481                self.advance();
4482                match self.peek().clone() {
4483                    Token::All => {
4484                        self.advance();
4485                        Ok(Statement::ResetParameter(None))
4486                    }
4487                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4488                        self.advance();
4489                        Ok(Statement::ResetParameter(None))
4490                    }
4491                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4492                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4493                        self.advance();
4494                        Ok(Statement::SetRole(None))
4495                    }
4496                    // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4497                    // (pg_dump's return from the owner switch).
4498                    Token::Ident(s) | Token::QuotedIdent(s)
4499                        if s.eq_ignore_ascii_case("session")
4500                            && matches!(
4501                                self.tokens.get(self.pos + 1),
4502                                Some(Token::Ident(a) | Token::QuotedIdent(a))
4503                                    if a.eq_ignore_ascii_case("authorization")
4504                            ) =>
4505                    {
4506                        self.advance(); // SESSION
4507                        self.advance(); // AUTHORIZATION
4508                        Ok(Statement::SetRole(None))
4509                    }
4510                    _ => {
4511                        let name = self.parse_set_param_name()?;
4512                        Ok(Statement::ResetParameter(Some(name)))
4513                    }
4514                }
4515            }
4516            // v7.39 (round 218) — server-side cursors.
4517            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4518            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4519            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4520            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4521                self.advance();
4522                match self.peek().clone() {
4523                    Token::All => {
4524                        self.advance();
4525                        Ok(Statement::CloseCursor { name: None })
4526                    }
4527                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4528                        self.advance();
4529                        Ok(Statement::CloseCursor { name: None })
4530                    }
4531                    Token::Ident(n) | Token::QuotedIdent(n) => {
4532                        self.advance();
4533                        Ok(Statement::CloseCursor { name: Some(n) })
4534                    }
4535                    other => Err(self.err(format!(
4536                        "expected cursor name or ALL after CLOSE, got {other:?}"
4537                    ))),
4538                }
4539            }
4540            other => Err(self.err(format!(
4541                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4542                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4543            ))),
4544        }
4545    }
4546
4547    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4548    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4549    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4550    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4551    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4552        self.advance(); // DECLARE
4553        let name = match self.advance() {
4554            Token::Ident(n) | Token::QuotedIdent(n) => n,
4555            other => {
4556                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4557            }
4558        };
4559        let mut scroll: Option<bool> = None;
4560        loop {
4561            match self.peek() {
4562                Token::Ident(s)
4563                    if s.eq_ignore_ascii_case("binary")
4564                        || s.eq_ignore_ascii_case("insensitive")
4565                        || s.eq_ignore_ascii_case("asensitive") =>
4566                {
4567                    self.advance();
4568                }
4569                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4570                    self.advance();
4571                    scroll = Some(true);
4572                }
4573                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4574                {
4575                    self.advance(); // NO
4576                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4577                        return Err(self.err(format!(
4578                            "expected SCROLL after NO in DECLARE, got {:?}",
4579                            self.peek()
4580                        )));
4581                    }
4582                    self.advance();
4583                    scroll = Some(false);
4584                }
4585                _ => break,
4586            }
4587        }
4588        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4589            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4590        }
4591        self.advance();
4592        let mut hold = false;
4593        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4594            self.advance();
4595            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4596                return Err(self.err(format!(
4597                    "expected HOLD after WITH in DECLARE, got {:?}",
4598                    self.peek()
4599                )));
4600            }
4601            self.advance();
4602            hold = true;
4603        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4604            self.advance();
4605            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4606                return Err(self.err(format!(
4607                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4608                    self.peek()
4609                )));
4610            }
4611            self.advance();
4612        }
4613        if !matches!(self.peek(), Token::For) {
4614            return Err(self.err(format!(
4615                "expected FOR before the cursor query, got {:?}",
4616                self.peek()
4617            )));
4618        }
4619        self.advance();
4620        let query = self.parse_one_statement()?;
4621        Ok(Statement::DeclareCursor {
4622            name,
4623            scroll,
4624            hold,
4625            query: alloc::boxed::Box::new(query),
4626        })
4627    }
4628
4629    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4630    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4631    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4632    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4633        use crate::ast::CursorDirection as D;
4634        self.advance(); // FETCH / MOVE
4635        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4636            let neg = if matches!(this.peek(), Token::Minus) {
4637                this.advance();
4638                true
4639            } else {
4640                false
4641            };
4642            match this.advance() {
4643                Token::Integer(v) => Ok(if neg { -v } else { v }),
4644                other => Err(this.err(format!("expected count, got {other:?}"))),
4645            }
4646        };
4647        let direction = match self.peek().clone() {
4648            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4649                self.advance();
4650                D::Next
4651            }
4652            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4653                self.advance();
4654                D::Prior
4655            }
4656            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4657                self.advance();
4658                D::First
4659            }
4660            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4661                self.advance();
4662                D::Last
4663            }
4664            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4665                self.advance();
4666                D::Absolute(signed_count(self)?)
4667            }
4668            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4669                self.advance();
4670                D::Relative(signed_count(self)?)
4671            }
4672            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4673                self.advance();
4674                match self.peek().clone() {
4675                    Token::All => {
4676                        self.advance();
4677                        D::All
4678                    }
4679                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4680                        self.advance();
4681                        D::All
4682                    }
4683                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4684                    _ => D::Next, // bare FORWARD = FORWARD 1
4685                }
4686            }
4687            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4688                self.advance();
4689                match self.peek().clone() {
4690                    Token::All => {
4691                        self.advance();
4692                        D::BackwardAll
4693                    }
4694                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4695                        self.advance();
4696                        D::BackwardAll
4697                    }
4698                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4699                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4700                }
4701            }
4702            Token::All => {
4703                self.advance();
4704                D::All
4705            }
4706            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4707                self.advance();
4708                D::All
4709            }
4710            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4711            // Bare `FETCH <name>` — direction defaults to NEXT.
4712            _ => D::Next,
4713        };
4714        // Optional FROM / IN.
4715        if matches!(self.peek(), Token::From)
4716            || matches!(self.peek(), Token::In)
4717            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4718        {
4719            self.advance();
4720        }
4721        let name = match self.advance() {
4722            Token::Ident(n) | Token::QuotedIdent(n) => n,
4723            other => {
4724                return Err(self.err(format!("expected cursor name, got {other:?}")));
4725            }
4726        };
4727        Ok(if is_move {
4728            Statement::MoveCursor { name, direction }
4729        } else {
4730            Statement::FetchCursor { name, direction }
4731        })
4732    }
4733
4734    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4735    /// [(kind, …)] ON <col>, … FROM <table>`.
4736    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4737        self.advance(); // STATISTICS
4738        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4739        let mut if_not_exists = false;
4740        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4741            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4742        {
4743            self.advance();
4744            self.advance();
4745            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4746                self.advance();
4747                if_not_exists = true;
4748            }
4749        }
4750        let name = self.expect_ident_like()?;
4751        let mut kinds = Vec::new();
4752        if matches!(self.peek(), Token::LParen) {
4753            self.advance();
4754            loop {
4755                let k = self.expect_ident_like()?;
4756                // PG stores the single letters; accept the spelled-out
4757                // names the SQL uses and record what PG records.
4758                kinds.push(match k.to_ascii_lowercase().as_str() {
4759                    "ndistinct" => String::from("d"),
4760                    "dependencies" => String::from("f"),
4761                    "mcv" => String::from("m"),
4762                    other => {
4763                        return Err(
4764                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4765                        );
4766                    }
4767                });
4768                match self.advance() {
4769                    Token::Comma => {}
4770                    Token::RParen => break,
4771                    other => {
4772                        return Err(self.err(alloc::format!(
4773                            "expected ',' or ')' in statistics kind list, got {other:?}"
4774                        )));
4775                    }
4776                }
4777            }
4778        }
4779        if !matches!(self.peek(), Token::On) {
4780            return Err(self.err(alloc::format!(
4781                "expected ON in CREATE STATISTICS, got {:?}",
4782                self.peek()
4783            )));
4784        }
4785        self.advance();
4786        let mut columns = Vec::new();
4787        loop {
4788            columns.push(self.expect_ident_like()?);
4789            if matches!(self.peek(), Token::Comma) {
4790                self.advance();
4791            } else {
4792                break;
4793            }
4794        }
4795        if !matches!(self.peek(), Token::From) {
4796            return Err(self.err(alloc::format!(
4797                "expected FROM in CREATE STATISTICS, got {:?}",
4798                self.peek()
4799            )));
4800        }
4801        self.advance();
4802        let table = self.expect_ident_like()?;
4803        Ok(Statement::CreateStatistics {
4804            name,
4805            if_not_exists,
4806            kinds,
4807            columns,
4808            table,
4809        })
4810    }
4811
4812    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4813    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4814    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4815    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4816    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4817    /// forward call.
4818    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4819        self.advance(); // TABLE
4820        let if_exists = self.consume_if_exists();
4821        let mut names: Vec<String> = Vec::new();
4822        loop {
4823            names.push(self.expect_ident_like()?);
4824            if matches!(self.peek(), Token::Comma) {
4825                self.advance();
4826                continue;
4827            }
4828            break;
4829        }
4830        if matches!(
4831            self.peek(),
4832            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4833                || s.eq_ignore_ascii_case("restrict")
4834        ) {
4835            self.advance();
4836        }
4837        Ok(Statement::DropTable { names, if_exists })
4838    }
4839
4840    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4841        self.advance(); // STATISTICS
4842        let mut if_exists = false;
4843        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4844            && matches!(self.tokens.get(self.pos + 1),
4845                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4846        {
4847            self.advance();
4848            self.advance();
4849            if_exists = true;
4850        }
4851        let name = self.expect_ident_like()?;
4852        Ok(Statement::DropStatistics { name, if_exists })
4853    }
4854
4855    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4856        debug_assert!(matches!(self.peek(), Token::Create));
4857        self.advance();
4858        match self.peek() {
4859            Token::Table => self.parse_create_table_stmt_after_create(),
4860            Token::Index => self.parse_create_index_stmt_after_create(false),
4861            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4862            // object now. It used to be consumed by the CREATE-noise
4863            // arm, so a pg_dump that declares extended statistics
4864            // restored silently without them and reflection showed
4865            // nothing.
4866            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4867                self.parse_create_statistics_after_create()
4868            }
4869            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4870            // The `UNIQUE` modifier turns a partial index into a
4871            // partial-uniqueness invariant (only rows matching the
4872            // WHERE predicate are checked for duplicates). mailrs
4873            // K1 (3 hits: email_templates default, calendar_events
4874            // master, calendar_events instance).
4875            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4876                self.advance();
4877                if !matches!(self.peek(), Token::Index) {
4878                    return Err(self.err(alloc::format!(
4879                        "expected INDEX after CREATE UNIQUE, got {:?}",
4880                        self.peek()
4881                    )));
4882                }
4883                self.parse_create_index_stmt_after_create(true)
4884            }
4885            Token::Publication => {
4886                self.advance();
4887                self.parse_create_publication_after_keyword()
4888            }
4889            Token::Subscription => {
4890                self.advance();
4891                self.parse_create_subscription_after_keyword()
4892            }
4893            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4894            // USER isn't a reserved keyword — we look for the bare
4895            // identifier so the lexer doesn't have to grow a token.
4896            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4897                self.advance();
4898                self.parse_create_user_after_keyword(true)
4899            }
4900            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4901            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4902            // the default of the LOGIN attribute.
4903            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4904                self.advance();
4905                self.parse_create_user_after_keyword(false)
4906            }
4907            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4908            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4909                self.advance();
4910                self.parse_create_policy_after_keyword()
4911            }
4912            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4913            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4914            // no-op. mailrs follow-up F3.
4915            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4916                self.advance();
4917                self.parse_create_extension_after_keyword()
4918            }
4919            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4920            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4921            // optional; absorb it here and forward to the
4922            // per-kind parsers with the flag. OR is a reserved
4923            // keyword token.
4924            Token::Or => {
4925                self.advance();
4926                let next = self.peek();
4927                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4928                    return Err(self.err(alloc::format!(
4929                        "expected REPLACE after CREATE OR, got {next:?}"
4930                    )));
4931                };
4932                if !s2.eq_ignore_ascii_case("replace") {
4933                    return Err(self.err(alloc::format!(
4934                        "expected REPLACE after CREATE OR, got {s2:?}"
4935                    )));
4936                }
4937                self.advance();
4938                self.parse_create_function_or_trigger_after_or_replace(true)
4939            }
4940            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4941                self.advance();
4942                self.parse_create_function_after_keyword(false)
4943            }
4944            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4945                self.advance();
4946                self.parse_create_trigger_after_keyword(false)
4947            }
4948            // v7.39 (round 139) — CREATE RULE …
4949            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4950                self.advance();
4951                self.parse_create_rule_after_keyword(false)
4952            }
4953            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4954            // trigger is a row-level AFTER trigger that additionally carries
4955            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4956            // path already tolerates and skips those clauses, so consuming the
4957            // CONSTRAINT keyword and reusing it makes the statement parse and the
4958            // trigger fire. (The deferral timing itself is not yet honoured —
4959            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4960            // for every non-deferred use.)
4961            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
4962                self.advance();
4963                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
4964                    if t.eq_ignore_ascii_case("trigger"))
4965                {
4966                    return Err(self.err(alloc::format!(
4967                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
4968                        self.peek()
4969                    )));
4970                }
4971                self.advance();
4972                self.parse_create_trigger_after_keyword(false)
4973            }
4974            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
4975            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
4976                self.advance();
4977                self.parse_create_sequence_after_keyword(false)
4978            }
4979            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
4980            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
4981                self.advance();
4982                self.parse_create_view_after_keyword(false, false, false)
4983            }
4984            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
4985            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
4986            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
4987            // appear (in any order) between `CREATE` and `VIEW` in
4988            // every mysqldump-emitted view. Pre-2.6 the parser
4989            // rejected the prefix and the customer's whole view
4990            // backup failed on the first view. The hints are pure
4991            // planner / permission metadata; SPG's view-rewrite
4992            // path is semantically equivalent for all three
4993            // algorithms in v7.17 (TEMPTABLE differs only in
4994            // perf for huge views — out of v7.17 scope), and
4995            // DEFINER / SQL SECURITY are pure single-user
4996            // permissioning that SPG ignores by design.
4997            Token::Ident(s) | Token::QuotedIdent(s)
4998                if s.eq_ignore_ascii_case("algorithm")
4999                    || s.eq_ignore_ascii_case("definer")
5000                    || s.eq_ignore_ascii_case("sql") =>
5001            {
5002                self.consume_mysql_view_prefix()?;
5003                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
5004                // (in any order, in any combination), the next
5005                // keyword must be VIEW. mysqldump never emits these
5006                // prefixes on non-view statements.
5007                let next = self.peek().clone();
5008                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
5009                    if s2.eq_ignore_ascii_case("view"))
5010                {
5011                    self.advance();
5012                    self.parse_create_view_after_keyword(false, false, false)
5013                } else {
5014                    Err(self.err(alloc::format!(
5015                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
5016                    )))
5017                }
5018            }
5019            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
5020            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
5021                self.advance();
5022                self.parse_create_type_after_keyword()
5023            }
5024            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
5025            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
5026            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
5027                self.advance();
5028                self.parse_create_domain_after_keyword()
5029            }
5030            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
5031            // name [AUTHORIZATION user]. Real catalog registry
5032            // (was silent-no-op'd pre-v7.17).
5033            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
5034                self.advance();
5035                let if_not_exists = self.parse_if_not_exists();
5036                let name = self.expect_ident_like()?;
5037                // Optional `AUTHORIZATION <user>` trailer — accepted,
5038                // ignored (single-user catalog).
5039                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5040                    if s.eq_ignore_ascii_case("authorization"))
5041                {
5042                    self.advance();
5043                    let _ = self.expect_ident_like()?;
5044                }
5045                Ok(Statement::CreateSchema { name, if_not_exists })
5046            }
5047            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5048            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5049                self.advance();
5050                let next = self.peek().clone();
5051                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5052                {
5053                    self.advance();
5054                    self.parse_create_materialized_view_after_keyword()
5055                } else {
5056                    Err(self.err(alloc::format!(
5057                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5058                    )))
5059                }
5060            }
5061            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5062            // no-op below), an UNLOGGED table is a real, fully-usable table in
5063            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5064            // durability optimisation is a follow-up), so a dump / app that
5065            // declares UNLOGGED tables works instead of failing to parse.
5066            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5067                self.advance(); // UNLOGGED
5068                if matches!(self.peek(), Token::Table) {
5069                    self.parse_create_table_stmt_after_create()
5070                } else {
5071                    Err(self.err(format!(
5072                        "expected TABLE after CREATE UNLOGGED, got {:?}",
5073                        self.peek()
5074                    )))
5075                }
5076            }
5077            Token::Ident(s) | Token::QuotedIdent(s)
5078                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5079            {
5080                self.advance();
5081                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5082                let next = self.peek().clone();
5083                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5084                {
5085                    self.advance();
5086                    self.parse_create_sequence_after_keyword(true)
5087                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5088                {
5089                    self.advance();
5090                    self.parse_create_view_after_keyword(false, false, true)
5091                } else {
5092                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5093                    // consumed and answered OK while creating nothing, so
5094                    // every statement that touched the table afterwards failed
5095                    // with "table not found" — the DDL itself lied. It is a
5096                    // real CREATE TABLE now, marked temporary so the executor
5097                    // puts it in the session's own namespace. An optional
5098                    // TABLE keyword may or may not be present (`CREATE TEMP t`
5099                    // is not legal, but the keyword is consumed by the
5100                    // CREATE TABLE parser itself).
5101                    let stmt = self.parse_create_table_stmt_after_create()?;
5102                    match stmt {
5103                        Statement::CreateTable(mut c) => {
5104                            c.temporary = true;
5105                            Ok(Statement::CreateTable(c))
5106                        }
5107                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5108                        // CTAS node, which needs the same session namespace.
5109                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5110                            m.temporary = true;
5111                            Ok(Statement::CreateMaterializedView(m))
5112                        }
5113                        other => Ok(other),
5114                    }
5115                }
5116            }
5117            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5118            // BEGIN <body> END`. The body may reference `@var`
5119            // session variables, SET statements, internal `;`
5120            // terminators, etc. SPG has no procedure runtime, so
5121            // consume the whole `CREATE PROCEDURE … END` block as
5122            // a no-op so mysqldump scripts that include stored
5123            // routines load through. The matching-END consumer
5124            // tracks BEGIN/END nesting depth to handle nested
5125            // BEGIN blocks correctly.
5126            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5127                self.consume_mysql_routine_body();
5128                Ok(Statement::Empty)
5129            }
5130            // v7.14.0 — pg_dump / mysqldump emit
5131            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5132            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5133            // SPG is single-schema / single-database; these have
5134            // no behavioural effect, so consume + return Empty.
5135            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5136            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5137            // moved up to real parser branches. DATABASE / ROLE /
5138            // POLICY / OPERATOR stay no-op forever
5139            // (single-database, hardcoded roles).
5140            Token::Ident(s) | Token::QuotedIdent(s)
5141                if matches!(
5142                    s.to_ascii_lowercase().as_str(),
5143                    "database"
5144                        | "role"
5145                        | "operator"
5146                        | "cast"
5147                        | "aggregate"
5148                        | "language"
5149                        | "collation"
5150                        | "conversion"
5151                        // v7.17.0 Phase 8 (audit N6) — rarely-
5152                        // emitted pg_dump shapes that should
5153                        // load through without a parser error.
5154                        // SPG has no planner statistics catalog,
5155                        // no event-trigger hooks, no foreign-
5156                        // data-wrapper infrastructure; consume
5157                        // + return Empty.
5158                        | "statistics"
5159                        | "event"
5160                        // v7.37.17 (17.6 siblings) — additional CREATE
5161                        // targets pg_dump / operator install scripts
5162                        // may emit that SPG has no matching machinery
5163                        // for. Consume + Empty-return.
5164                        | "text"
5165                        | "tablespace"
5166                        | "access"
5167                        | "large"
5168                ) =>
5169            {
5170                // DATABASE is the one member of this list PG refuses
5171                // inside a transaction block; the rest (ROLE, CAST,
5172                // TABLESPACE, …) it runs there quite happily, so only
5173                // this one is named. Still a no-op otherwise — SPG is
5174                // single-database.
5175                let is_database = s.eq_ignore_ascii_case("database");
5176                // The name is the first token after DATABASE, past an
5177                // `IF NOT EXISTS`.
5178                let name = if is_database {
5179                    self.scan_database_name()
5180                } else {
5181                    None
5182                };
5183                let collation = if is_database {
5184                    self.scan_database_collation_until_boundary()
5185                } else {
5186                    self.consume_until_statement_boundary();
5187                    None
5188                };
5189                if is_database {
5190                    return Ok(Statement::NoOpPreventedInTransaction {
5191                        what: String::from("CREATE DATABASE"),
5192                        collation,
5193                        name,
5194                    });
5195                }
5196                Ok(Statement::Empty)
5197            }
5198            // v7.39 (round 706) — the foreign-data family leaves the silent
5199            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5200            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5201            // FDW machinery), but the ENGINE now warns, so a restore log
5202            // says what will not function instead of reporting success.
5203            Token::Ident(s) | Token::QuotedIdent(s)
5204                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5205            {
5206                self.consume_until_statement_boundary();
5207                Ok(Statement::ValidateOnly {
5208                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5209                    names: Vec::new(),
5210                })
5211            }
5212            other => Err(self.err(format!(
5213                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5214            ))),
5215        }
5216    }
5217
5218    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5219    /// keyword decides whether we parse a function or trigger
5220    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5221    /// PROCEDURE) — those land in later releases.
5222    fn parse_create_function_or_trigger_after_or_replace(
5223        &mut self,
5224        or_replace: bool,
5225    ) -> Result<Statement, ParseError> {
5226        let tok = self.peek();
5227        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5228            return Err(self.err(alloc::format!(
5229                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5230            )));
5231        };
5232        if s.eq_ignore_ascii_case("function") {
5233            self.advance();
5234            self.parse_create_function_after_keyword(or_replace)
5235        } else if s.eq_ignore_ascii_case("trigger") {
5236            self.advance();
5237            self.parse_create_trigger_after_keyword(or_replace)
5238        } else if s.eq_ignore_ascii_case("rule") {
5239            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5240            self.advance();
5241            self.parse_create_rule_after_keyword(or_replace)
5242        } else if s.eq_ignore_ascii_case("view") {
5243            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5244            self.advance();
5245            self.parse_create_view_after_keyword(or_replace, false, false)
5246        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5247            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5248            self.advance();
5249            let nxt = self.peek().clone();
5250            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5251            {
5252                self.advance();
5253                self.parse_create_view_after_keyword(or_replace, false, true)
5254            } else {
5255                Err(self.err(alloc::format!(
5256                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5257                )))
5258            }
5259        } else {
5260            Err(self.err(alloc::format!(
5261                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5262            )))
5263        }
5264    }
5265
5266    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5267    /// SPG doesn't have a registry; pgvector / similar are
5268    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5269    /// the syntax lets dual-target schemas keep the line.
5270    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5271        // Optional `IF NOT EXISTS`.
5272        self.consume_if_not_exists();
5273        let name = self.expect_ident_like()?;
5274        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5275        // CASCADE / FROM '<v>' clauses; we don't model them.
5276        loop {
5277            match self.peek() {
5278                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5279                    self.advance();
5280                    continue;
5281                }
5282                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5283                    self.advance();
5284                    let _ = self.expect_ident_like()?;
5285                    continue;
5286                }
5287                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5288                    self.advance();
5289                    // String or ident literal.
5290                    let _ = self.advance();
5291                    continue;
5292                }
5293                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5294                    self.advance();
5295                    let _ = self.advance();
5296                    continue;
5297                }
5298                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5299                    self.advance();
5300                    continue;
5301                }
5302                _ => break,
5303            }
5304        }
5305        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5306        // nosuch` reported success and `pg_extension` then did not list it,
5307        // which is the accept-and-do-nothing shape F31 exists to find.
5308        Ok(Statement::ValidateOnly {
5309            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5310            names: alloc::vec![name],
5311        })
5312    }
5313
5314    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5315    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5316    /// already been consumed by the caller. Grammar accepted:
5317    ///
5318    ///   name `(` arg-list `)`
5319    ///   `RETURNS` return-type
5320    ///   [ `LANGUAGE` ident ]
5321    ///   `AS` $$ body $$
5322    ///   [ `LANGUAGE` ident ]
5323    ///
5324    /// Either `LANGUAGE` position is allowed; PG accepts both.
5325    fn parse_create_function_after_keyword(
5326        &mut self,
5327        or_replace: bool,
5328    ) -> Result<Statement, ParseError> {
5329        let name = self.expect_ident_like()?;
5330        // Argument list. v7.12.4 commonly sees the empty `()`
5331        // (trigger functions); typed args parse and round-trip
5332        // but the executor only invokes nullary functions.
5333        if !matches!(self.peek(), Token::LParen) {
5334            return Err(self.err(alloc::format!(
5335                "expected '(' after function name {name:?}, got {:?}",
5336                self.peek()
5337            )));
5338        }
5339        self.advance();
5340        let args = self.parse_function_arg_list()?;
5341        // RETURNS clause.
5342        let tok = self.peek();
5343        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5344            return Err(self.err(alloc::format!(
5345                "expected RETURNS after function arg list, got {tok:?}"
5346            )));
5347        };
5348        if !s.eq_ignore_ascii_case("returns") {
5349            return Err(self.err(alloc::format!(
5350                "expected RETURNS after function arg list, got {s:?}"
5351            )));
5352        }
5353        self.advance();
5354        let returns = self.parse_function_return()?;
5355        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5356        // re-check after the body too).
5357        let mut language: Option<String> = self.parse_optional_language()?;
5358        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5359        // either side of the body and in any order, interleaved with
5360        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5361        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5362        // PG's own pg_dump output did not restore.
5363        let mut attrs = FunctionAttrs::default();
5364        loop {
5365            let before = self.pos;
5366            self.parse_function_attrs_into(&mut attrs)?;
5367            if language.is_none() {
5368                language = self.parse_optional_language()?;
5369            }
5370            if self.pos == before {
5371                break;
5372            }
5373        }
5374        // `AS` followed by a $$-quoted body (lexer already
5375        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5376        // Token::String). AS is a reserved keyword (Token::As).
5377        if !matches!(self.peek(), Token::As) {
5378            return Err(self.err(alloc::format!(
5379                "expected AS before function body, got {:?}",
5380                self.peek()
5381            )));
5382        }
5383        self.advance();
5384        let body_text = match self.peek() {
5385            Token::String(s) => {
5386                let body = s.clone();
5387                self.advance();
5388                body
5389            }
5390            other => {
5391                return Err(self.err(alloc::format!(
5392                    "expected $$-quoted function body after AS, got {other:?}"
5393                )));
5394            }
5395        };
5396        // Trailing clauses — PG's other accepted position for both the
5397        // LANGUAGE and the attributes.
5398        loop {
5399            let before = self.pos;
5400            self.parse_function_attrs_into(&mut attrs)?;
5401            if language.is_none() {
5402                language = self.parse_optional_language()?;
5403            }
5404            if self.pos == before {
5405                break;
5406            }
5407        }
5408        let language = language.unwrap_or_else(|| String::from("sql"));
5409        // PL/pgSQL bodies get structure-parsed. Other languages
5410        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5411        // recognise) round-trip as Raw text — the executor errors
5412        // when invoked with a clear unsupported message.
5413        let body = if language.eq_ignore_ascii_case("plpgsql") {
5414            match parse_plpgsql_body(&body_text) {
5415                Ok(block) => FunctionBody::PlPgSql(block),
5416                // Best-effort: if the body parser doesn't yet
5417                // support a construct used inside, fall back to
5418                // raw — keeps `CREATE FUNCTION` itself working
5419                // (catalogue accepts), executor errors on
5420                // invocation only.
5421                Err(_) => FunctionBody::Raw(body_text),
5422            }
5423        } else {
5424            FunctionBody::Raw(body_text)
5425        };
5426        Ok(Statement::CreateFunction(CreateFunctionStatement {
5427            name,
5428            or_replace,
5429            args,
5430            returns,
5431            language,
5432            body,
5433            attrs,
5434        }))
5435    }
5436
5437    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5438    /// attribute clauses into `attrs`, stopping at the first token that
5439    /// is not one. Measured against PG 18.4, which accepts them in any
5440    /// order and on either side of the body.
5441    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5442        loop {
5443            let word = match self.peek() {
5444                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5445                // NOT LEAKPROOF — NOT is a reserved keyword token.
5446                Token::Not
5447                    if matches!(
5448                        self.tokens.get(self.pos + 1),
5449                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5450                    ) =>
5451                {
5452                    self.advance();
5453                    self.advance();
5454                    attrs.leakproof = false;
5455                    continue;
5456                }
5457                _ => return Ok(()),
5458            };
5459            match word.as_str() {
5460                "immutable" => {
5461                    self.advance();
5462                    attrs.volatility = FunctionVolatility::Immutable;
5463                }
5464                "stable" => {
5465                    self.advance();
5466                    attrs.volatility = FunctionVolatility::Stable;
5467                }
5468                "volatile" => {
5469                    self.advance();
5470                    attrs.volatility = FunctionVolatility::Volatile;
5471                }
5472                "strict" => {
5473                    self.advance();
5474                    attrs.strict = true;
5475                }
5476                "leakproof" => {
5477                    self.advance();
5478                    attrs.leakproof = true;
5479                }
5480                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5481                // spelled-out forms of STRICT and its opposite.
5482                "returns" | "called" => {
5483                    let strict = word == "returns";
5484                    let mut probe = self.pos + 1;
5485                    if strict {
5486                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5487                        // is not ours.
5488                        match self.tokens.get(probe) {
5489                            Some(Token::Null) => probe += 1,
5490                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5491                            _ => return Ok(()),
5492                        }
5493                    }
5494                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5495                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5496                    if !ok {
5497                        return Ok(());
5498                    }
5499                    probe += 1;
5500                    match self.tokens.get(probe) {
5501                        Some(Token::Null) => probe += 1,
5502                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5503                        _ => return Ok(()),
5504                    }
5505                    match self.tokens.get(probe) {
5506                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5507                        _ => return Ok(()),
5508                    }
5509                    self.pos = probe;
5510                    attrs.strict = strict;
5511                }
5512                "security" | "external" => {
5513                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5514                    let mut probe = self.pos + 1;
5515                    if word == "external" {
5516                        match self.tokens.get(probe) {
5517                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5518                                probe += 1;
5519                            }
5520                            _ => return Ok(()),
5521                        }
5522                    }
5523                    let definer = match self.tokens.get(probe) {
5524                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5525                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5526                        _ => return Ok(()),
5527                    };
5528                    self.pos = probe + 1;
5529                    attrs.security_definer = definer;
5530                }
5531                "parallel" => {
5532                    let level = match self.tokens.get(self.pos + 1) {
5533                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5534                            FunctionParallel::Safe
5535                        }
5536                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5537                            FunctionParallel::Restricted
5538                        }
5539                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5540                            FunctionParallel::Unsafe
5541                        }
5542                        _ => return Ok(()),
5543                    };
5544                    self.pos += 2;
5545                    attrs.parallel = level;
5546                }
5547                "cost" | "rows" => {
5548                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5549                        return Ok(());
5550                    };
5551                    self.pos += 2;
5552                    if word == "cost" {
5553                        attrs.cost = Some(n);
5554                    } else {
5555                        attrs.rows = Some(n);
5556                    }
5557                }
5558                _ => return Ok(()),
5559            }
5560        }
5561    }
5562
5563    /// The numeric literal at `idx`, if there is one.
5564    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5565        match self.tokens.get(idx)? {
5566            Token::Integer(n) => Some(*n as f64),
5567            Token::Float(f) => Some(*f),
5568            Token::Numeric(t) => t.parse::<f64>().ok(),
5569            _ => None,
5570        }
5571    }
5572
5573    /// Closing `)`-terminated argument list. v7.12.4 commonly
5574    /// sees the empty `()`; typed args round-trip but the
5575    /// executor (yet) doesn't invoke them.
5576    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5577    /// it away, which is what PG does with one on a function parameter.
5578    fn skip_type_modifier(&mut self) {
5579        if !matches!(self.peek(), Token::LParen) {
5580            return;
5581        }
5582        // Only a numeric modifier — anything else is not one, and eating
5583        // it would swallow real grammar.
5584        let mut i = self.pos + 1;
5585        let mut seen_number = false;
5586        loop {
5587            match self.tokens.get(i) {
5588                Some(Token::Integer(_)) => seen_number = true,
5589                Some(Token::Comma) => {}
5590                Some(Token::RParen) => break,
5591                _ => return,
5592            }
5593            i += 1;
5594        }
5595        if !seen_number {
5596            return;
5597        }
5598        while self.pos <= i {
5599            self.advance();
5600        }
5601    }
5602
5603    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5604        let mut args: Vec<FunctionArg> = Vec::new();
5605        if matches!(self.peek(), Token::RParen) {
5606            self.advance();
5607            return Ok(args);
5608        }
5609        loop {
5610            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5611            // a reserved token; OUT / INOUT are bare idents.
5612            let mode = if matches!(self.peek(), Token::In) {
5613                self.advance();
5614                FunctionArgMode::In
5615            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5616            {
5617                self.advance();
5618                FunctionArgMode::Out
5619            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5620            {
5621                self.advance();
5622                FunctionArgMode::InOut
5623            } else {
5624                FunctionArgMode::In
5625            };
5626            // Optional name. The next token is either a name
5627            // (followed by a type ident) or the type itself.
5628            // Disambiguate by peeking ahead: if the token after
5629            // the next ident is also an ident, we treat the
5630            // first as the name.
5631            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5632            // the comma or paren, then decide. Reading at most two of
5633            // them could not spell `x double precision` at all, and
5634            // silently mis-read the bare `double precision` as a
5635            // parameter named "double" — which is what made the same
5636            // signature key two different ways.
5637            let (name, ty_token) = {
5638                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5639                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5640                    words.push(self.expect_ident_like()?);
5641                }
5642                // v7.39 (round 344) — a length / precision modifier on the
5643                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5644                // accepts it and DROPS it — `pg_get_function_arguments`
5645                // reports plain `character varying` / `numeric`, measured on
5646                // 18.4 — but SPG raised `syntax error at or near "("`,
5647                // because the modifier's parens were never consumed.
5648                self.skip_type_modifier();
5649                // r1049 — `f(v bigint[])`. The array suffix parsed in
5650                // the column position, the cast position and (r1038)
5651                // the RETURNS position, but not here: the fifth
5652                // member of the same family, reported by sentori as
5653                // presumably the same code. It is now.
5654                let array_suffix = self.consume_array_suffix();
5655                let whole = words.join(" ");
5656                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5657                {
5658                    (Some(words[0].clone()), words[1..].join(" "))
5659                } else {
5660                    (None, whole)
5661                };
5662                ty_token.push_str(&array_suffix);
5663                (name, ty_token)
5664            };
5665            // Type — try to map to ColumnTypeName, else Raw.
5666            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5667                Some(t) => FunctionArgType::Typed(t),
5668                None => FunctionArgType::Raw(ty_token),
5669            };
5670            args.push(FunctionArg { mode, name, ty });
5671            match self.peek() {
5672                Token::Comma => {
5673                    self.advance();
5674                    continue;
5675                }
5676                Token::RParen => {
5677                    self.advance();
5678                    return Ok(args);
5679                }
5680                other => {
5681                    return Err(self.err(alloc::format!(
5682                        "expected , or ) in function arg list, got {other:?}"
5683                    )));
5684                }
5685            }
5686        }
5687    }
5688
5689    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5690        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5691        // function whose row shape is named inline.
5692        if matches!(self.peek(), Token::Table)
5693            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5694        {
5695            self.advance(); // TABLE
5696            self.advance(); // (
5697            let mut cols: Vec<String> = Vec::new();
5698            loop {
5699                let cname = self.expect_ident_like()?;
5700                let mut ty: Vec<String> = Vec::new();
5701                loop {
5702                    match self.peek() {
5703                        Token::Comma | Token::RParen | Token::Eof => break,
5704                        _ => {}
5705                    }
5706                    match self.advance() {
5707                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5708                        other => {
5709                            if let Some(w) = unreserved_keyword_text(&other) {
5710                                ty.push(w);
5711                            }
5712                        }
5713                    }
5714                }
5715                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5716                if matches!(self.peek(), Token::Comma) {
5717                    self.advance();
5718                } else {
5719                    break;
5720                }
5721            }
5722            if matches!(self.peek(), Token::RParen) {
5723                self.advance();
5724            }
5725            return Ok(FunctionReturn::Other(alloc::format!(
5726                "TABLE({})",
5727                cols.join(", ")
5728            )));
5729        }
5730        let ident = self.expect_ident_like()?;
5731        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5732        if ident.eq_ignore_ascii_case("setof") {
5733            let inner = self.expect_ident_like()?;
5734            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5735            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5736        }
5737        if ident.eq_ignore_ascii_case("trigger") {
5738            return Ok(FunctionReturn::Trigger);
5739        }
5740        if ident.eq_ignore_ascii_case("void") {
5741            return Ok(FunctionReturn::Void);
5742        }
5743        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5744        // RETURN position did not, so the `[` was a syntax error and the
5745        // whole migration stopped. sentori worked around it by returning
5746        // zero-padded text.
5747        let suffix = self.consume_array_suffix();
5748        if !suffix.is_empty() {
5749            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5750        }
5751        match map_type_ident_to_column_type_name(&ident) {
5752            Some(t) => Ok(FunctionReturn::Type(t)),
5753            None => Ok(FunctionReturn::Other(ident)),
5754        }
5755    }
5756
5757    /// Consume any `[]` / `[N]` array markers after a type name and give
5758    /// back their text. Empty when there are none.
5759    fn consume_array_suffix(&mut self) -> String {
5760        let mut out = String::new();
5761        while matches!(self.peek(), Token::LBracket) {
5762            self.advance();
5763            // `[N]` is accepted and, as in PG, the length is not enforced.
5764            if let Token::Integer(n) = self.peek().clone() {
5765                self.advance();
5766                out.push_str(&alloc::format!("[{n}]"));
5767            } else {
5768                out.push_str("[]");
5769            }
5770            if matches!(self.peek(), Token::RBracket) {
5771                self.advance();
5772            }
5773        }
5774        out
5775    }
5776
5777    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5778        match self.peek() {
5779            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5780                self.advance();
5781                let lang = self.expect_ident_like()?;
5782                Ok(Some(lang.to_ascii_lowercase()))
5783            }
5784            _ => Ok(None),
5785        }
5786    }
5787
5788    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5789    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5790    /// (expr)]*`. The `DOMAIN` keyword has already been
5791    /// consumed. PG allows the trailing constraints in any
5792    /// order; we approximate with a small loop.
5793    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5794        let name = self.expect_ident_like()?;
5795        // Optional `AS`.
5796        if matches!(self.peek(), Token::As) {
5797            self.advance();
5798        }
5799        // v7.39 (round 259) — keep the raw type NAME when the base is not
5800        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5801        // parent domain.
5802        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5803            self.parse_type_with_implied_flags()?;
5804        let mut default: Option<Expr> = None;
5805        let mut not_null = false;
5806        let mut checks: Vec<Expr> = Vec::new();
5807        loop {
5808            match self.peek() {
5809                Token::Default => {
5810                    if default.is_some() {
5811                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5812                    }
5813                    self.advance();
5814                    default = Some(self.parse_expr(0)?);
5815                }
5816                Token::Not => {
5817                    self.advance();
5818                    if !matches!(self.peek(), Token::Null) {
5819                        return Err(self.err(alloc::format!(
5820                            "expected NULL after NOT in DOMAIN, got {:?}",
5821                            self.peek()
5822                        )));
5823                    }
5824                    self.advance();
5825                    not_null = true;
5826                }
5827                Token::Null => {
5828                    self.advance();
5829                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5830                    // is the default-nullable marker (PG accepts it),
5831                    // but AFTER a NOT NULL it is a conflict PG refuses
5832                    // (`conflicting NULL/NOT NULL constraints`,
5833                    // PG18-measured); the old arm no-opped both ways.
5834                    if not_null {
5835                        return Err(self.err(alloc::string::String::from(
5836                            "conflicting NULL/NOT NULL constraints",
5837                        )));
5838                    }
5839                }
5840                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5841                    self.advance();
5842                    if !matches!(self.peek(), Token::LParen) {
5843                        return Err(self.err(alloc::format!(
5844                            "expected '(' after CHECK in DOMAIN, got {:?}",
5845                            self.peek()
5846                        )));
5847                    }
5848                    self.advance();
5849                    let expr = self.parse_expr(0)?;
5850                    if !matches!(self.peek(), Token::RParen) {
5851                        return Err(self.err(alloc::format!(
5852                            "expected ')' after CHECK expr, got {:?}",
5853                            self.peek()
5854                        )));
5855                    }
5856                    self.advance();
5857                    checks.push(expr);
5858                }
5859                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5860                // prefix on the constraint; we drop the name and
5861                // recurse into the constraint parsing.
5862                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5863                    self.advance();
5864                    let _ = self.expect_ident_like()?;
5865                }
5866                _ => break,
5867            }
5868        }
5869        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5870            name,
5871            base_type,
5872            base_domain: base_user_ref,
5873            default,
5874            not_null,
5875            checks,
5876        }))
5877    }
5878
5879    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5880    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5881    /// consumed.
5882    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5883        let name = self.expect_ident_like()?;
5884        // Required `AS`.
5885        if !matches!(self.peek(), Token::As) {
5886            return Err(self.err(alloc::format!(
5887                "expected AS after CREATE TYPE {name:?}, got {:?}",
5888                self.peek()
5889            )));
5890        }
5891        self.advance();
5892        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5893        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5894        // on the next token: `(` = composite, ident `ENUM` = enum.
5895        if matches!(self.peek(), Token::LParen) {
5896            self.advance();
5897            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5898            let mut field_user_types: Vec<Option<String>> = Vec::new();
5899            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5900            // is legal PG (an attribute-less composite; measured — the old
5901            // e2e note claimed PG requires at least one attribute).
5902            if matches!(self.peek(), Token::RParen) {
5903                self.advance();
5904                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5905                    name,
5906                    kind: crate::ast::TypeKind::Composite {
5907                        fields,
5908                        field_user_types,
5909                    },
5910                }));
5911            }
5912            loop {
5913                let field_name = self.expect_ident_like()?;
5914                // v7.39 (round 264) — keep the raw type name when it is not
5915                // a builtin: that is how a NESTED composite field records
5916                // which composite it holds.
5917                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
5918                    self.parse_type_with_implied_flags()?;
5919                fields.push((field_name, field_type));
5920                field_user_types.push(field_user_ref);
5921                if matches!(self.peek(), Token::Comma) {
5922                    self.advance();
5923                    continue;
5924                }
5925                if matches!(self.peek(), Token::RParen) {
5926                    self.advance();
5927                    break;
5928                }
5929                return Err(self.err(alloc::format!(
5930                    "expected , or ) in composite field list, got {:?}",
5931                    self.peek()
5932                )));
5933            }
5934            if fields.is_empty() {
5935                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5936            }
5937            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5938                name,
5939                kind: crate::ast::TypeKind::Composite {
5940                    fields,
5941                    field_user_types,
5942                },
5943            }));
5944        }
5945        // Required `ENUM` ident.
5946        let kind_ident = match self.peek().clone() {
5947            Token::Ident(s) | Token::QuotedIdent(s) => s,
5948            other => {
5949                return Err(self.err(alloc::format!(
5950                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5951                )));
5952            }
5953        };
5954        if !kind_ident.eq_ignore_ascii_case("enum") {
5955            return Err(self.err(alloc::format!(
5956                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5957            )));
5958        }
5959        self.advance();
5960        if !matches!(self.peek(), Token::LParen) {
5961            return Err(self.err(alloc::format!(
5962                "expected '(' after ENUM, got {:?}",
5963                self.peek()
5964            )));
5965        }
5966        self.advance();
5967        let mut labels: Vec<String> = Vec::new();
5968        loop {
5969            match self.peek().clone() {
5970                Token::String(s) => {
5971                    self.advance();
5972                    labels.push(s);
5973                }
5974                other => {
5975                    return Err(
5976                        self.err(alloc::format!("expected enum label string, got {other:?}"))
5977                    );
5978                }
5979            }
5980            if matches!(self.peek(), Token::Comma) {
5981                self.advance();
5982                continue;
5983            }
5984            if matches!(self.peek(), Token::RParen) {
5985                self.advance();
5986                break;
5987            }
5988            return Err(self.err(alloc::format!(
5989                "expected , or ) in ENUM label list, got {:?}",
5990                self.peek()
5991            )));
5992        }
5993        if labels.is_empty() {
5994            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
5995        }
5996        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5997            name,
5998            kind: crate::ast::TypeKind::Enum { labels },
5999        }))
6000    }
6001
6002    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
6003    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
6004    /// The `CREATE MATERIALIZED VIEW` keywords have already been
6005    /// consumed.
6006    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
6007        let if_not_exists = self.parse_if_not_exists();
6008        let name = self.expect_ident_like()?;
6009        let mut columns: Vec<String> = Vec::new();
6010        if matches!(self.peek(), Token::LParen) {
6011            self.advance();
6012            loop {
6013                let c = self.expect_ident_like()?;
6014                columns.push(c);
6015                if matches!(self.peek(), Token::Comma) {
6016                    self.advance();
6017                    continue;
6018                }
6019                if matches!(self.peek(), Token::RParen) {
6020                    self.advance();
6021                    break;
6022                }
6023                return Err(self.err(alloc::format!(
6024                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
6025                    self.peek()
6026                )));
6027            }
6028        }
6029        if !matches!(self.peek(), Token::As) {
6030            return Err(self.err(alloc::format!(
6031                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
6032                self.peek()
6033            )));
6034        }
6035        self.advance();
6036        // v7.39 (round 151) — a WITH-headed body is legal (read-only
6037        // CTEs only; the engine rejects data-modifying ones with PG's
6038        // message). A trailing `WITH [NO] DATA` can't START the body,
6039        // so WITH here heads the query.
6040        let body = if self.peek_is_with_kw() {
6041            self.advance();
6042            self.parse_nested_with_select()?
6043        } else {
6044            let body_stmt = self.parse_select_stmt()?;
6045            let Statement::Select(body) = body_stmt else {
6046                return Err(self.err(alloc::format!(
6047                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6048                )));
6049            };
6050            body
6051        };
6052        // Optional trailing `WITH [NO] DATA`.
6053        let with_data = self.parse_optional_with_data(true)?;
6054        Ok(Statement::CreateMaterializedView(
6055            crate::ast::CreateMaterializedViewStatement {
6056                temporary: false,
6057                name,
6058                if_not_exists,
6059                columns,
6060                body,
6061                with_data,
6062                as_plain_table: false,
6063            },
6064        ))
6065    }
6066
6067    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6068    /// `default_when_absent` is what to return if the tail is
6069    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6070    /// WITH DATA).
6071    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6072        let save = self.pos;
6073        // `WITH` is an Ident (not reserved in the lexer).
6074        let is_with = match self.peek() {
6075            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6076            _ => false,
6077        };
6078        if !is_with {
6079            return Ok(default_when_absent);
6080        }
6081        self.advance();
6082        // Optional `NO`.
6083        let mut with_data = true;
6084        let is_no = match self.peek() {
6085            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6086            _ => false,
6087        };
6088        if is_no {
6089            self.advance();
6090            with_data = false;
6091        }
6092        // Required `DATA` ident.
6093        let is_data = match self.peek() {
6094            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6095            _ => false,
6096        };
6097        if is_data {
6098            self.advance();
6099            Ok(with_data)
6100        } else {
6101            // Caller's WITH wasn't WITH-DATA — rewind so the outer
6102            // parser can interpret it.
6103            self.pos = save;
6104            Ok(default_when_absent)
6105        }
6106    }
6107
6108    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6109    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6110    /// All keyword prefixes have already been consumed; the flags
6111    /// say which were present.
6112    fn parse_create_view_after_keyword(
6113        &mut self,
6114        or_replace: bool,
6115        _materialized_unused: bool,
6116        temporary: bool,
6117    ) -> Result<Statement, ParseError> {
6118        let if_not_exists = self.parse_if_not_exists();
6119        let name = self.expect_ident_like()?;
6120        // Optional `(col, col, …)` rename list.
6121        let mut columns: Vec<String> = Vec::new();
6122        if matches!(self.peek(), Token::LParen) {
6123            self.advance();
6124            loop {
6125                let c = self.expect_ident_like()?;
6126                columns.push(c);
6127                if matches!(self.peek(), Token::Comma) {
6128                    self.advance();
6129                    continue;
6130                }
6131                if matches!(self.peek(), Token::RParen) {
6132                    self.advance();
6133                    break;
6134                }
6135                return Err(self.err(alloc::format!(
6136                    "expected , or ) in VIEW column list, got {:?}",
6137                    self.peek()
6138                )));
6139            }
6140        }
6141        // Required `AS`.
6142        if !matches!(self.peek(), Token::As) {
6143            return Err(self.err(alloc::format!(
6144                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6145                self.peek()
6146            )));
6147        }
6148        self.advance();
6149        // Body: a regular SELECT statement. v7.39 (round 151) — a
6150        // WITH-headed body is legal too (read-only CTEs only; the
6151        // engine rejects data-modifying ones with PG's message).
6152        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6153        // with the check-option clause, so WITH here heads the query.
6154        let body = if self.peek_is_with_kw() {
6155            self.advance();
6156            self.parse_nested_with_select()?
6157        } else {
6158            let body_stmt = self.parse_select_stmt()?;
6159            let Statement::Select(body) = body_stmt else {
6160                return Err(self.err(alloc::format!(
6161                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6162                )));
6163            };
6164            body
6165        };
6166        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6167        // The SELECT parser stops before a trailing WITH, so it lands here.
6168        let check_option = if matches!(self.peek(),
6169            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6170        {
6171            self.advance(); // WITH
6172            let opt = match self.peek() {
6173                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6174                    self.advance();
6175                    crate::ast::ViewCheckOption::Local
6176                }
6177                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6178                    self.advance();
6179                    crate::ast::ViewCheckOption::Cascaded
6180                }
6181                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6182                _ => crate::ast::ViewCheckOption::Cascaded,
6183            };
6184            if !matches!(self.peek(),
6185                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6186            {
6187                return Err(self.err(alloc::format!(
6188                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6189                    self.peek()
6190                )));
6191            }
6192            self.advance(); // CHECK
6193            if !matches!(self.peek(),
6194                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6195            {
6196                return Err(self.err(alloc::format!(
6197                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6198                    self.peek()
6199                )));
6200            }
6201            self.advance(); // OPTION
6202            Some(opt)
6203        } else {
6204            None
6205        };
6206        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6207            name,
6208            or_replace,
6209            if_not_exists,
6210            temporary,
6211            columns,
6212            body,
6213            check_option,
6214        }))
6215    }
6216
6217    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6218    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6219    /// consumed; `temporary` carries whether TEMPORARY was seen.
6220    fn parse_create_sequence_after_keyword(
6221        &mut self,
6222        temporary: bool,
6223    ) -> Result<Statement, ParseError> {
6224        let if_not_exists = self.parse_if_not_exists();
6225        let name = self.expect_ident_like()?;
6226        // Optional `AS data_type`.
6227        let data_type = if matches!(self.peek(), Token::As) {
6228            self.advance();
6229            Some(self.parse_sequence_data_type()?)
6230        } else {
6231            None
6232        };
6233        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6234        Ok(Statement::CreateSequence(
6235            crate::ast::CreateSequenceStatement {
6236                name,
6237                if_not_exists,
6238                temporary,
6239                data_type,
6240                options,
6241            },
6242        ))
6243    }
6244
6245    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6246    /// already been consumed; this is reached after `SEQUENCE`.
6247    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6248    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6249        use crate::ast::AlterDomainAction as A;
6250        let name = self.expect_ident_like()?;
6251        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6252        let kw = match self.peek() {
6253            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6254            Token::Drop => alloc::string::String::from("drop"),
6255            Token::Default => alloc::string::String::from("default"),
6256            other => {
6257                return Err(self.err(alloc::format!(
6258                    "expected an ALTER DOMAIN action, got {other:?}"
6259                )));
6260            }
6261        };
6262        let action = match kw.as_str() {
6263            "add" => {
6264                self.advance();
6265                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6266                {
6267                    self.advance();
6268                    Some(self.expect_ident_like()?)
6269                } else {
6270                    None
6271                };
6272                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6273                    return Err(self.err(alloc::format!(
6274                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6275                        self.peek()
6276                    )));
6277                }
6278                self.advance();
6279                if !matches!(self.peek(), Token::LParen) {
6280                    return Err(self.err("expected '(' after CHECK".into()));
6281                }
6282                self.advance();
6283                let check = self.parse_expr(0)?;
6284                if !matches!(self.peek(), Token::RParen) {
6285                    return Err(self.err("expected ')' after CHECK expression".into()));
6286                }
6287                self.advance();
6288                A::AddConstraint { name: cname, check }
6289            }
6290            "drop" => {
6291                self.advance();
6292                match self.peek() {
6293                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6294                        self.advance();
6295                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6296                        {
6297                            self.advance();
6298                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6299                            {
6300                                return Err(self.err("expected EXISTS after IF".into()));
6301                            }
6302                            self.advance();
6303                            true
6304                        } else {
6305                            false
6306                        };
6307                        let cn = self.expect_ident_like()?;
6308                        A::DropConstraint {
6309                            name: cn,
6310                            if_exists,
6311                        }
6312                    }
6313                    Token::Default => {
6314                        self.advance();
6315                        A::DropDefault
6316                    }
6317                    Token::Not => {
6318                        self.advance();
6319                        if !matches!(self.peek(), Token::Null) {
6320                            return Err(self.err("expected NULL after NOT".into()));
6321                        }
6322                        self.advance();
6323                        A::DropNotNull
6324                    }
6325                    other => {
6326                        return Err(self.err(alloc::format!(
6327                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6328                        )));
6329                    }
6330                }
6331            }
6332            "set" => {
6333                self.advance();
6334                match self.peek() {
6335                    Token::Default => {
6336                        self.advance();
6337                        A::SetDefault(self.parse_expr(0)?)
6338                    }
6339                    Token::Not => {
6340                        self.advance();
6341                        if !matches!(self.peek(), Token::Null) {
6342                            return Err(self.err("expected NULL after NOT".into()));
6343                        }
6344                        self.advance();
6345                        A::SetNotNull
6346                    }
6347                    other => {
6348                        return Err(self.err(alloc::format!(
6349                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6350                        )));
6351                    }
6352                }
6353            }
6354            "rename" => {
6355                self.advance();
6356                if !matches!(self.peek(), Token::To) {
6357                    return Err(self.err("expected TO after RENAME".into()));
6358                }
6359                self.advance();
6360                A::RenameTo(self.expect_ident_like()?)
6361            }
6362            other => {
6363                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6364            }
6365        };
6366        Ok(Statement::AlterDomain { name, action })
6367    }
6368
6369    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6370        let if_exists = self.parse_if_exists();
6371        let name = self.expect_ident_like()?;
6372        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6373        // the option list (PG allows only one or the other).
6374        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6375            self.advance();
6376            if matches!(self.peek(), Token::To) {
6377                self.advance();
6378            } else {
6379                self.expect_keyword_ident("to")?;
6380            }
6381            let new = self.expect_ident_like()?;
6382            return Ok(Statement::AlterSequence(
6383                crate::ast::AlterSequenceStatement {
6384                    name,
6385                    if_exists,
6386                    options: crate::ast::SequenceOptions::default(),
6387                    rename_to: Some(new),
6388                },
6389            ));
6390        }
6391        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6392        Ok(Statement::AlterSequence(
6393            crate::ast::AlterSequenceStatement {
6394                name,
6395                if_exists,
6396                options,
6397                rename_to: None,
6398            },
6399        ))
6400    }
6401
6402    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6403        let kw = self.expect_ident_like()?;
6404        match kw.to_ascii_lowercase().as_str() {
6405            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6406            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6407            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6408            other => Err(self.err(alloc::format!(
6409                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6410            ))),
6411        }
6412    }
6413
6414    fn parse_sequence_options(
6415        &mut self,
6416        allow_restart: bool,
6417    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6418        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6419        let mut opts = SequenceOptions::default();
6420        #[allow(clippy::while_let_loop)]
6421        loop {
6422            // Match an ident; stop at any non-ident token (sentinel,
6423            // semicolon, end of statement).
6424            let kw_lc = match self.peek() {
6425                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6426                _ => break,
6427            };
6428            match kw_lc.as_str() {
6429                "increment" => {
6430                    self.advance();
6431                    // Optional BY.
6432                    if self.peek_is_by() {
6433                        self.advance();
6434                    }
6435                    opts.increment = Some(self.expect_signed_int()?);
6436                }
6437                "minvalue" => {
6438                    self.advance();
6439                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6440                }
6441                "maxvalue" => {
6442                    self.advance();
6443                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6444                }
6445                "no" => {
6446                    self.advance();
6447                    let what = self.expect_ident_like()?;
6448                    match what.to_ascii_lowercase().as_str() {
6449                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6450                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6451                        "cycle" => opts.cycle = Some(false),
6452                        other => {
6453                            return Err(self.err(alloc::format!(
6454                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6455                            )));
6456                        }
6457                    }
6458                }
6459                "start" => {
6460                    self.advance();
6461                    // Optional WITH.
6462                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6463                        if s.eq_ignore_ascii_case("with"))
6464                    {
6465                        self.advance();
6466                    }
6467                    opts.start = Some(self.expect_signed_int()?);
6468                }
6469                "restart" if allow_restart => {
6470                    self.advance();
6471                    // Optional WITH n; bare RESTART means restart at START.
6472                    let mut with_val: Option<i64> = None;
6473                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6474                        if s.eq_ignore_ascii_case("with"))
6475                    {
6476                        self.advance();
6477                        with_val = Some(self.expect_signed_int()?);
6478                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6479                        with_val = Some(self.expect_signed_int()?);
6480                    }
6481                    opts.restart = Some(with_val);
6482                }
6483                "cache" => {
6484                    self.advance();
6485                    opts.cache = Some(self.expect_signed_int()?);
6486                }
6487                "cycle" => {
6488                    self.advance();
6489                    opts.cycle = Some(true);
6490                }
6491                "owned" => {
6492                    self.advance();
6493                    match self.peek() {
6494                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6495                            self.advance();
6496                        }
6497                        other => {
6498                            return Err(
6499                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6500                            );
6501                        }
6502                    }
6503                    // OWNED BY {NONE | tab.col}. Read just one ident
6504                    // (NOT expect_ident_like which would auto-strip
6505                    // a schema prefix and consume the `.col` we need).
6506                    let first = match self.advance() {
6507                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6508                        other => {
6509                            return Err(self.err(alloc::format!(
6510                                "expected identifier or NONE after OWNED BY, got {other:?}"
6511                            )));
6512                        }
6513                    };
6514                    if first.eq_ignore_ascii_case("none") {
6515                        opts.owned_by = Some(SequenceOwnedBy::None);
6516                    } else if matches!(self.peek(), Token::Dot) {
6517                        self.advance();
6518                        let second = match self.advance() {
6519                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6520                            other => {
6521                                return Err(self.err(alloc::format!(
6522                                    "expected column name after OWNED BY {first}., got {other:?}"
6523                                )));
6524                            }
6525                        };
6526                        // v7.17 dump-compat fix — pg_dump emits
6527                        // OWNED BY clauses as
6528                        // `schema.table.column` (three segments).
6529                        // If a third `.<ident>` follows, treat the
6530                        // first ident as schema (drop it; SPG is
6531                        // single-schema) and the middle / last
6532                        // pair as table.column. Otherwise it's
6533                        // the two-segment form table.column.
6534                        if matches!(self.peek(), Token::Dot) {
6535                            self.advance();
6536                            let third = match self.advance() {
6537                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6538                                other => {
6539                                    return Err(self.err(alloc::format!(
6540                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6541                                    )));
6542                                }
6543                            };
6544                            let _ = first; // schema prefix discarded
6545                            opts.owned_by = Some(SequenceOwnedBy::Column {
6546                                table: second,
6547                                column: third,
6548                            });
6549                        } else {
6550                            opts.owned_by = Some(SequenceOwnedBy::Column {
6551                                table: first,
6552                                column: second,
6553                            });
6554                        }
6555                    } else {
6556                        return Err(self.err(alloc::format!(
6557                            "expected table.column or NONE after OWNED BY, got {first:?}"
6558                        )));
6559                    }
6560                }
6561                _ => break,
6562            }
6563        }
6564        Ok(opts)
6565    }
6566
6567    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6568        let neg = if matches!(self.peek(), Token::Minus) {
6569            self.advance();
6570            true
6571        } else {
6572            false
6573        };
6574        match self.peek() {
6575            Token::Integer(n) => {
6576                let v = *n;
6577                self.advance();
6578                Ok(if neg { -v } else { v })
6579            }
6580            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6581        }
6582    }
6583
6584    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6585    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6586    /// clause is fully accepted and discarded — SPG always runs
6587    /// constraint checks immediately (single-writer model). The
6588    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6589    /// in either order (per the SQL spec they're independent),
6590    /// though pg_dump always emits them in the canonical
6591    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6592    /// Stops at the first token that isn't part of the clause.
6593    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6594        self.consume_deferrable_clauses_timed().map(|_| ())
6595    }
6596
6597    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6598    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6599    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6600    /// NOT DEFERRABLE and a circular-FK migration could not load.
6601    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6602        let mut deferrable = false;
6603        let mut initially_deferred = false;
6604        loop {
6605            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6606            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6607                self.advance();
6608                deferrable = true;
6609                if self.consume_optional_initially_clause()? {
6610                    initially_deferred = true;
6611                }
6612                continue;
6613            }
6614            // `NOT DEFERRABLE` — already worked pre-3.1.
6615            if matches!(self.peek(), Token::Not) {
6616                let look = self.tokens.get(self.pos + 1);
6617                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6618                    self.advance(); // NOT
6619                    self.advance(); // DEFERRABLE
6620                    deferrable = false;
6621                    initially_deferred = false;
6622                    let _ = self.consume_optional_initially_clause()?;
6623                    continue;
6624                }
6625                break;
6626            }
6627            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6628            // accepts this without a leading [NOT] DEFERRABLE
6629            // (the timing keyword alone). pg_dump occasionally
6630            // emits it on FK constraints that inherit timing.
6631            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6632                if self.consume_optional_initially_clause()? {
6633                    initially_deferred = true;
6634                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6635                    deferrable = true;
6636                }
6637                continue;
6638            }
6639            break;
6640        }
6641        Ok((deferrable, initially_deferred))
6642    }
6643
6644    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6645    /// next token is `INITIALLY`, consume it plus the required
6646    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6647    /// Returns true when the timing seen was `DEFERRED`.
6648    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6649        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6650            return Ok(false);
6651        }
6652        self.advance(); // INITIALLY
6653        match self.advance() {
6654            Token::Ident(s)
6655                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6656            {
6657                Ok(s.eq_ignore_ascii_case("deferred"))
6658            }
6659            other => Err(self.err(alloc::format!(
6660                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6661            ))),
6662        }
6663    }
6664
6665    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6666    /// in its entirety so the parser returns Empty without
6667    /// touching the runtime. The CREATE+PROCEDURE keywords are
6668    /// already consumed; this swallows everything from the
6669    /// procedure name through the matching `END`, including
6670    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6671    /// (DELIMITER `//` makes the script splitter forward the
6672    /// whole block as one statement), `@var` session-variable
6673    /// references, and the trailing terminator.
6674    ///
6675    /// Tracks nesting depth so:
6676    ///   BEGIN
6677    ///     IF cond THEN
6678    ///       BEGIN ... END;
6679    ///     END IF;
6680    ///   END
6681    /// terminates at the outer END.
6682    fn consume_mysql_routine_body(&mut self) {
6683        // Outer skeleton: name, (...), optional clauses, BEGIN
6684        // <body> END [;]. Scan for the first BEGIN — anything
6685        // before it is signature decoration we don't care about.
6686        // Once inside BEGIN, count up on BEGIN, down on END.
6687        let mut depth: i32 = 0;
6688        let mut started = false;
6689        loop {
6690            match self.peek().clone() {
6691                Token::Begin => {
6692                    self.advance();
6693                    depth += 1;
6694                    started = true;
6695                }
6696                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6697                    self.advance();
6698                    if started {
6699                        depth -= 1;
6700                        if depth <= 0 {
6701                            // Optional trailing ident (`END IF`,
6702                            // `END LOOP`, `END WHILE`, `END CASE`,
6703                            // `END label_name`) — eat the next
6704                            // ident if present so we don't
6705                            // mistake `END IF;` for the outer
6706                            // close.
6707                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6708                                // If the next token is one of the
6709                                // PL/SQL block-closer keywords,
6710                                // the END belongs to an inner
6711                                // block; bump depth back up.
6712                                let is_inner_close = matches!(
6713                                    self.peek(),
6714                                    Token::Ident(s) | Token::QuotedIdent(s)
6715                                        if matches!(
6716                                            s.to_ascii_lowercase().as_str(),
6717                                            "if" | "loop" | "while" | "case" | "repeat"
6718                                        )
6719                                );
6720                                if is_inner_close {
6721                                    self.advance();
6722                                    depth += 1;
6723                                    continue;
6724                                }
6725                            }
6726                            // Eat optional trailing `;`.
6727                            if matches!(self.peek(), Token::Semicolon) {
6728                                self.advance();
6729                            }
6730                            return;
6731                        }
6732                    }
6733                }
6734                Token::Eof => return,
6735                _ => {
6736                    self.advance();
6737                }
6738            }
6739        }
6740    }
6741
6742    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6743    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6744    ///
6745    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6746    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6747    ///   ident, or `ident @ ident-or-quoted-string` host form)
6748    /// * `SQL SECURITY {DEFINER|INVOKER}`
6749    ///
6750    /// Each clause may appear at most once but in any order.
6751    /// The hints are pure planner / permission metadata that
6752    /// SPG's view-rewrite engine handles uniformly; we accept
6753    /// and discard. Returns `Ok(())` once a non-clause token is
6754    /// peeked (the caller then checks for the `VIEW` keyword).
6755    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6756        loop {
6757            match self.peek().clone() {
6758                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6759                    self.advance(); // ALGORITHM
6760                    // Optional `=`. MySQL spec requires it but be
6761                    // generous.
6762                    if matches!(self.peek(), Token::Eq) {
6763                        self.advance();
6764                    }
6765                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6766                    // bare ident; unknown values still parse so
6767                    // future MySQL versions don't break.
6768                    if matches!(
6769                        self.peek(),
6770                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6771                    ) {
6772                        self.advance();
6773                    }
6774                }
6775                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6776                    self.advance(); // DEFINER
6777                    if matches!(self.peek(), Token::Eq) {
6778                        self.advance();
6779                    }
6780                    // User: quoted string, ident, OR ident @ host
6781                    // (host may itself be quoted or bare).
6782                    match self.peek().clone() {
6783                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6784                            self.advance();
6785                            // Optional `@host`.
6786                            if matches!(self.peek(), Token::At) {
6787                                self.advance();
6788                                if matches!(
6789                                    self.peek(),
6790                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6791                                ) {
6792                                    self.advance();
6793                                }
6794                            }
6795                        }
6796                        _ => {}
6797                    }
6798                }
6799                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6800                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6801                    // when followed by SECURITY — the dispatcher must
6802                    // not consume a bare `SQL` token (it's not a
6803                    // legal CREATE prefix on its own).
6804                    let save = self.pos;
6805                    self.advance(); // SQL
6806                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6807                        if s2.eq_ignore_ascii_case("security"))
6808                    {
6809                        self.advance(); // SECURITY
6810                        // DEFINER / INVOKER trailing ident.
6811                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6812                            self.advance();
6813                        }
6814                    } else {
6815                        // Not a SQL SECURITY clause — roll back and
6816                        // bail; the caller will error out cleanly.
6817                        self.pos = save;
6818                        return Ok(());
6819                    }
6820                }
6821                _ => return Ok(()),
6822            }
6823        }
6824    }
6825
6826    fn parse_if_not_exists(&mut self) -> bool {
6827        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6828        {
6829            let save = self.pos;
6830            self.advance();
6831            if matches!(self.peek(), Token::Not) {
6832                self.advance();
6833                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6834                {
6835                    self.advance();
6836                    return true;
6837                }
6838            }
6839            self.pos = save;
6840        }
6841        false
6842    }
6843
6844    fn parse_if_exists(&mut self) -> bool {
6845        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6846        {
6847            let save = self.pos;
6848            self.advance();
6849            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6850            {
6851                self.advance();
6852                return true;
6853            }
6854            self.pos = save;
6855        }
6856        false
6857    }
6858
6859    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6860    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6861    /// been consumed.
6862    fn parse_create_trigger_after_keyword(
6863        &mut self,
6864        or_replace: bool,
6865    ) -> Result<Statement, ParseError> {
6866        let name = self.expect_ident_like()?;
6867        let timing = {
6868            let ident = self.expect_ident_like()?;
6869            if ident.eq_ignore_ascii_case("before") {
6870                TriggerTiming::Before
6871            } else if ident.eq_ignore_ascii_case("after") {
6872                TriggerTiming::After
6873            } else if ident.eq_ignore_ascii_case("instead") {
6874                let next = self.expect_ident_like()?;
6875                if !next.eq_ignore_ascii_case("of") {
6876                    return Err(self.err(alloc::format!(
6877                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6878                    )));
6879                }
6880                TriggerTiming::InsteadOf
6881            } else {
6882                return Err(self.err(alloc::format!(
6883                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6884                )));
6885            }
6886        };
6887        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6888        // OR is a reserved keyword token (Token::Or), not an Ident.
6889        // v7.13.0 — after an UPDATE event we may optionally see
6890        // `OF col, col, …` (mailrs round-5 G7). Columns are
6891        // captured into `update_columns` once across the whole
6892        // events list; multiple `UPDATE OF` clauses are rejected.
6893        let mut events: Vec<TriggerEvent> = Vec::new();
6894        let mut update_columns: Vec<String> = Vec::new();
6895        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6896        events.push(first_ev);
6897        if !first_cols.is_empty() {
6898            update_columns = first_cols;
6899        }
6900        while matches!(self.peek(), Token::Or) {
6901            self.advance();
6902            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6903            events.push(ev);
6904            if !cols.is_empty() {
6905                if !update_columns.is_empty() {
6906                    return Err(
6907                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6908                    );
6909                }
6910                update_columns = cols;
6911            }
6912        }
6913        // ON <table>
6914        let tok = self.peek();
6915        let Token::On = tok else {
6916            return Err(self.err(alloc::format!(
6917                "expected ON after trigger events, got {tok:?}"
6918            )));
6919        };
6920        self.advance();
6921        let table = self.expect_ident_like()?;
6922        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6923        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6924        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6925        // the trigger as a plain AFTER trigger (correct for every non-deferred
6926        // use; deferral timing is not yet honoured).
6927        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6928            if s.eq_ignore_ascii_case("from"))
6929        {
6930            self.advance();
6931            let _reftable = self.expect_ident_like()?;
6932        }
6933        self.consume_optional_deferrable_clauses()?;
6934        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6935        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6936        // idents.
6937        if !matches!(self.peek(), Token::For) {
6938            return Err(self.err(alloc::format!(
6939                "expected FOR EACH ROW / STATEMENT, got {:?}",
6940                self.peek()
6941            )));
6942        }
6943        self.advance();
6944        let for_each = {
6945            let e = self.expect_ident_like()?;
6946            if !e.eq_ignore_ascii_case("each") {
6947                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6948            }
6949            let unit = self.expect_ident_like()?;
6950            if unit.eq_ignore_ascii_case("row") {
6951                TriggerForEach::Row
6952            } else if unit.eq_ignore_ascii_case("statement") {
6953                TriggerForEach::Statement
6954            } else {
6955                return Err(self.err(alloc::format!(
6956                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6957                )));
6958            }
6959        };
6960        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6961        let when_condition = if matches!(self.peek(),
6962            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
6963        {
6964            self.advance();
6965            Some(self.parse_paren_expr("WHEN")?)
6966        } else {
6967            None
6968        };
6969        // EXECUTE FUNCTION/PROCEDURE name(...)
6970        let exec = self.expect_ident_like()?;
6971        if !exec.eq_ignore_ascii_case("execute") {
6972            return Err(self.err(alloc::format!(
6973                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
6974            )));
6975        }
6976        let fn_or_proc = self.expect_ident_like()?;
6977        if !(fn_or_proc.eq_ignore_ascii_case("function")
6978            || fn_or_proc.eq_ignore_ascii_case("procedure"))
6979        {
6980            return Err(self.err(alloc::format!(
6981                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
6982            )));
6983        }
6984        let function = self.expect_ident_like()?;
6985        // Optional empty arg list `()`.
6986        if matches!(self.peek(), Token::LParen) {
6987            self.advance();
6988            if !matches!(self.peek(), Token::RParen) {
6989                return Err(self.err(alloc::format!(
6990                    "v7.12.4 trigger function calls take no args; got {:?}",
6991                    self.peek()
6992                )));
6993            }
6994            self.advance();
6995        }
6996        Ok(Statement::CreateTrigger(CreateTriggerStatement {
6997            name,
6998            or_replace,
6999            timing,
7000            events,
7001            table,
7002            for_each,
7003            function,
7004            update_columns,
7005            when_condition,
7006        }))
7007    }
7008
7009    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
7010    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
7011    fn parse_create_rule_after_keyword(
7012        &mut self,
7013        or_replace: bool,
7014    ) -> Result<Statement, ParseError> {
7015        let name = self.expect_ident_like()?;
7016        if !matches!(self.peek(), Token::As) {
7017            return Err(self.err(alloc::format!(
7018                "expected AS in CREATE RULE, got {:?}",
7019                self.peek()
7020            )));
7021        }
7022        self.advance();
7023        if !matches!(self.peek(), Token::On) {
7024            return Err(self.err(alloc::format!(
7025                "expected ON in CREATE RULE, got {:?}",
7026                self.peek()
7027            )));
7028        }
7029        self.advance();
7030        let event = self.parse_rule_event()?;
7031        if !matches!(self.peek(), Token::To)
7032            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
7033        {
7034            return Err(self.err(alloc::format!(
7035                "expected TO after rule event, got {:?}",
7036                self.peek()
7037            )));
7038        }
7039        self.advance();
7040        let table = self.expect_ident_like()?;
7041        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7042        let when_condition = if matches!(self.peek(), Token::Where) {
7043            self.advance();
7044            Some(self.parse_expr(0)?)
7045        } else {
7046            None
7047        };
7048        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7049        {
7050            return Err(self.err(alloc::format!(
7051                "expected DO in CREATE RULE, got {:?}",
7052                self.peek()
7053            )));
7054        }
7055        self.advance();
7056        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7057        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7058        {
7059            self.advance();
7060            true
7061        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7062            self.advance();
7063            false
7064        } else {
7065            false
7066        };
7067        // `NOTHING` | `( cmd; … )` | `cmd`.
7068        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7069        {
7070            self.advance();
7071            Vec::new()
7072        } else if matches!(self.peek(), Token::LParen) {
7073            self.advance();
7074            let mut cmds = Vec::new();
7075            loop {
7076                cmds.push(self.parse_one_statement()?);
7077                if matches!(self.peek(), Token::Semicolon) {
7078                    self.advance();
7079                    if matches!(self.peek(), Token::RParen) {
7080                        break;
7081                    }
7082                    continue;
7083                }
7084                break;
7085            }
7086            if !matches!(self.peek(), Token::RParen) {
7087                return Err(self.err(alloc::format!(
7088                    "expected ) closing the CREATE RULE command list, got {:?}",
7089                    self.peek()
7090                )));
7091            }
7092            self.advance();
7093            cmds
7094        } else {
7095            alloc::vec![self.parse_one_statement()?]
7096        };
7097        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7098            name,
7099            or_replace,
7100            event,
7101            table,
7102            instead,
7103            when_condition,
7104            commands,
7105        }))
7106    }
7107
7108    /// v7.39 (round 139) — a rule event keyword → uppercase string.
7109    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7110        if matches!(self.peek(), Token::Insert) {
7111            self.advance();
7112            return Ok(alloc::string::String::from("INSERT"));
7113        }
7114        if matches!(self.peek(), Token::Select) {
7115            self.advance();
7116            return Ok(alloc::string::String::from("SELECT"));
7117        }
7118        match self.peek() {
7119            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7120                self.advance();
7121                Ok(alloc::string::String::from("UPDATE"))
7122            }
7123            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7124                self.advance();
7125                Ok(alloc::string::String::from("DELETE"))
7126            }
7127            other => Err(self.err(alloc::format!(
7128                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7129            ))),
7130        }
7131    }
7132
7133    /// v7.13.0 — parse one trigger event, then optionally consume
7134    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7135    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7136    fn parse_trigger_event_with_optional_of(
7137        &mut self,
7138    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7139        let ev = self.parse_trigger_event()?;
7140        if !matches!(ev, TriggerEvent::Update) {
7141            return Ok((ev, Vec::new()));
7142        }
7143        // `OF` is a bare ident.
7144        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7145            return Ok((ev, Vec::new()));
7146        }
7147        self.advance(); // OF
7148        let mut cols: Vec<String> = Vec::new();
7149        loop {
7150            cols.push(self.expect_ident_like()?);
7151            if matches!(self.peek(), Token::Comma) {
7152                self.advance();
7153                continue;
7154            }
7155            break;
7156        }
7157        if cols.is_empty() {
7158            return Err(
7159                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7160            );
7161        }
7162        Ok((ev, cols))
7163    }
7164
7165    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7166    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7167    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7168    /// inside the body.
7169    /// Called by [`parse_plpgsql_body`] after the body's tokens
7170    /// have been lexed into this temporary parser.
7171    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7172        // v7.12.6 — optional DECLARE prelude.
7173        let declarations = if matches!(
7174            self.peek(),
7175            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7176        ) {
7177            self.advance();
7178            self.parse_plpgsql_declare_block()?
7179        } else {
7180            Vec::new()
7181        };
7182        // BEGIN keyword (PL/pgSQL — distinct from the SQL
7183        // `BEGIN` transaction-start, but we can reuse the
7184        // reserved Token::Begin since the body is a separate
7185        // lex/parse context).
7186        if !matches!(self.peek(), Token::Begin) {
7187            return Err(self.err(alloc::format!(
7188                "expected BEGIN at start of plpgsql block, got {:?}",
7189                self.peek()
7190            )));
7191        }
7192        self.advance();
7193        let statements = self.parse_plpgsql_stmt_list_until_end()?;
7194        // v7.37.20 (20.10) — optional EXCEPTION clause between the
7195        // body's last statement and the trailing END. When present
7196        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7197        // arms terminated by END.
7198        let exception_handlers = if matches!(
7199            self.peek(),
7200            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7201        ) {
7202            self.advance();
7203            self.parse_plpgsql_exception_handlers()?
7204        } else {
7205            Vec::new()
7206        };
7207        Ok(PlPgSqlBlock {
7208            declarations,
7209            statements,
7210            exception_handlers,
7211        })
7212    }
7213
7214    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7215    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7216    fn parse_plpgsql_exception_handlers(
7217        &mut self,
7218    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7219        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7220        loop {
7221            // Stop at END — the block-level trailing END LOOP / END;
7222            // is handled by the caller.
7223            if matches!(
7224                self.peek(),
7225                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7226            ) {
7227                return Ok(out);
7228            }
7229            // WHEN <cond> [OR <cond>]* THEN <body>
7230            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7231            {
7232                return Err(self.err(alloc::format!(
7233                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
7234                    self.peek()
7235                )));
7236            }
7237            self.advance();
7238            let mut conditions: Vec<String> = Vec::new();
7239            conditions.push(self.expect_ident_like()?);
7240            while matches!(self.peek(), Token::Or) {
7241                self.advance();
7242                conditions.push(self.expect_ident_like()?);
7243            }
7244            let then_kw = self.expect_ident_like()?;
7245            if !then_kw.eq_ignore_ascii_case("then") {
7246                return Err(self.err(alloc::format!(
7247                    "expected THEN after WHEN condition list, got {then_kw:?}"
7248                )));
7249            }
7250            let body = self.parse_plpgsql_stmt_list_until_end()?;
7251            out.push(crate::ast::ExceptionHandler { conditions, body });
7252        }
7253    }
7254
7255    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7256    /// prelude. Caller has already consumed `DECLARE`. We stop
7257    /// reading entries when we hit `BEGIN`.
7258    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7259        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7260        loop {
7261            if matches!(self.peek(), Token::Begin) {
7262                return Ok(out);
7263            }
7264            let name = self.expect_ident_like()?;
7265            // v7.37.20 (20.7) — type inference: if the next token is
7266            // `:=` or `=` (no explicit type), infer from the default
7267            // expression. Otherwise the ident that follows is the
7268            // declared type.
7269            //
7270            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7271            // (PG-standard). SPG parse-accepts and treats identically
7272            // to inference — the eventual runtime value determines
7273            // the local's type, which is faithful to how SPG handles
7274            // untyped locals today (see 20.7). Full compile-time
7275            // catalog lookup queues with v7.40 PL/pgSQL epic.
7276            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7277                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7278                // downstream declaration walker to type the local by
7279                // the runtime type of the default expression.
7280                FunctionArgType::Raw("_infer_".into())
7281            } else {
7282                let ty_token = self.expect_ident_like()?;
7283                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7284                // consume optional `.<ident>` qualifier + `%<KW>`
7285                // suffix. Both qualifier and suffix map to _infer_.
7286                if matches!(self.peek(), Token::Dot) {
7287                    self.advance();
7288                    let _ = self.expect_ident_like()?;
7289                }
7290                if matches!(self.peek(), Token::Percent) {
7291                    self.advance();
7292                    // Consume the trailing TYPE / ROWTYPE ident.
7293                    let _ = self.expect_ident_like()?;
7294                    FunctionArgType::Raw("_infer_".into())
7295                } else {
7296                    match map_type_ident_to_column_type_name(&ty_token) {
7297                        Some(t) => FunctionArgType::Typed(t),
7298                        None => FunctionArgType::Raw(ty_token),
7299                    }
7300                }
7301            };
7302            let default = match self.peek() {
7303                Token::ColonEq => {
7304                    self.advance();
7305                    Some(self.parse_expr(0)?)
7306                }
7307                Token::Eq => {
7308                    // PL/pgSQL also accepts `=` for the
7309                    // DECLARE default (PG treats them the same
7310                    // in this position).
7311                    self.advance();
7312                    Some(self.parse_expr(0)?)
7313                }
7314                _ => None,
7315            };
7316            // Mandatory `;` between declarations.
7317            if !matches!(self.peek(), Token::Semicolon) {
7318                return Err(self.err(alloc::format!(
7319                    "expected ; after DECLARE entry for {name:?}, got {:?}",
7320                    self.peek()
7321                )));
7322            }
7323            self.advance();
7324            out.push(PlPgSqlDeclare { name, ty, default });
7325        }
7326    }
7327
7328    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7329    /// the terminating `END;` (or `END IF;` etc — handled by the
7330    /// per-construct sub-parsers). Used by both the outer block
7331    /// and the IF/ELSE branch bodies.
7332    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7333        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7334        loop {
7335            // Allow trailing semicolons + END.
7336            while matches!(self.peek(), Token::Semicolon) {
7337                self.advance();
7338            }
7339            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7340            if matches!(
7341                self.peek(),
7342                Token::Ident(s) | Token::QuotedIdent(s)
7343                    if s.eq_ignore_ascii_case("end")
7344                        || s.eq_ignore_ascii_case("else")
7345                        || s.eq_ignore_ascii_case("elsif")
7346                        || s.eq_ignore_ascii_case("elseif")
7347                        || s.eq_ignore_ascii_case("exception")
7348                        || s.eq_ignore_ascii_case("when")
7349            ) {
7350                return Ok(statements);
7351            }
7352            // Otherwise: one statement, then expect `;` or
7353            // a block-terminator keyword.
7354            let stmt = self.parse_plpgsql_stmt()?;
7355            statements.push(stmt);
7356            match self.peek() {
7357                Token::Semicolon => {
7358                    self.advance();
7359                }
7360                Token::Ident(s) | Token::QuotedIdent(s)
7361                    if s.eq_ignore_ascii_case("end")
7362                        || s.eq_ignore_ascii_case("else")
7363                        || s.eq_ignore_ascii_case("elsif")
7364                        || s.eq_ignore_ascii_case("elseif")
7365                        || s.eq_ignore_ascii_case("exception")
7366                        || s.eq_ignore_ascii_case("when") =>
7367                {
7368                    // Final statement of the block without `;`.
7369                }
7370                other => {
7371                    return Err(self.err(alloc::format!(
7372                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7373                    )));
7374                }
7375            }
7376        }
7377    }
7378
7379    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7380        // RETURN keyword?
7381        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7382        {
7383            self.advance();
7384            return self.parse_plpgsql_return();
7385        }
7386        // v7.12.6 — IF block.
7387        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7388        {
7389            self.advance();
7390            return self.parse_plpgsql_if();
7391        }
7392        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7393        // Detected by peeking that token pos+3 is Ident("execute").
7394        if matches!(self.peek(), Token::For)
7395            && matches!(
7396                self.tokens.get(self.pos + 1),
7397                Some(Token::Ident(_) | Token::QuotedIdent(_))
7398            )
7399            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7400            && matches!(
7401                self.tokens.get(self.pos + 3),
7402                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7403            )
7404        {
7405            self.advance(); // FOR
7406            let var = self.expect_ident_like()?;
7407            self.advance(); // IN
7408            self.advance(); // EXECUTE
7409            // Prescan for LOOP at paren depth 0 so parse_expr stops
7410            // before the LOOP keyword (same trick as the bare-SELECT
7411            // ForQuery arm).
7412            let mut depth: i32 = 0;
7413            let mut loop_pos: Option<usize> = None;
7414            let mut scan = self.pos;
7415            while scan < self.tokens.len() {
7416                match self.tokens.get(scan) {
7417                    Some(Token::LParen) => depth += 1,
7418                    Some(Token::RParen) => depth -= 1,
7419                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7420                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7421                    {
7422                        loop_pos = Some(scan);
7423                        break;
7424                    }
7425                    _ => {}
7426                }
7427                scan += 1;
7428            }
7429            let loop_pos = loop_pos.ok_or_else(|| {
7430                self.err(alloc::format!(
7431                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7432                ))
7433            })?;
7434            let saved_loop = self.tokens[loop_pos].clone();
7435            self.tokens[loop_pos] = Token::Semicolon;
7436            let expr_result = self.parse_expr(0);
7437            self.tokens[loop_pos] = saved_loop;
7438            let sql_expr = expr_result?;
7439            let loop_kw = self.expect_ident_like()?;
7440            if !loop_kw.eq_ignore_ascii_case("loop") {
7441                return Err(self.err(alloc::format!(
7442                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7443                )));
7444            }
7445            let body = self.parse_plpgsql_stmt_list_until_end()?;
7446            let end_kw = self.expect_ident_like()?;
7447            if !end_kw.eq_ignore_ascii_case("end") {
7448                return Err(self.err(alloc::format!(
7449                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7450                )));
7451            }
7452            let loop_kw2 = self.expect_ident_like()?;
7453            if !loop_kw2.eq_ignore_ascii_case("loop") {
7454                return Err(self.err(alloc::format!(
7455                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7456                )));
7457            }
7458            return Ok(PlPgSqlStmt::ForExecute {
7459                var,
7460                sql_expr,
7461                body,
7462            });
7463        }
7464        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7465        //
7466        // Two syntactic forms:
7467        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7468        //   FOR var IN (SELECT ...) LOOP ...
7469        //
7470        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7471        // the trailing `LOOP` keyword as a table alias, we prescan
7472        // forward to find LOOP at paren depth 0, splice a fake
7473        // Semicolon at that position (so SELECT parses cleanly),
7474        // then re-splice LOOP back in.
7475        //
7476        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7477        // LOOP directly — no scan required.
7478        if matches!(self.peek(), Token::For)
7479            && matches!(
7480                self.tokens.get(self.pos + 1),
7481                Some(Token::Ident(_) | Token::QuotedIdent(_))
7482            )
7483            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7484            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7485                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7486        {
7487            self.advance(); // FOR
7488            let var = self.expect_ident_like()?;
7489            // IN
7490            self.advance();
7491            let query = if matches!(self.peek(), Token::LParen) {
7492                // Paren-wrapped SELECT.
7493                self.advance();
7494                let inner = self.parse_select_stmt()?;
7495                let Statement::Select(q) = inner else {
7496                    return Err(self.err(alloc::format!(
7497                        "expected SELECT inside (…), got {:?}",
7498                        self.peek()
7499                    )));
7500                };
7501                if !matches!(self.peek(), Token::RParen) {
7502                    return Err(self.err(alloc::format!(
7503                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7504                        self.peek()
7505                    )));
7506                }
7507                self.advance();
7508                q
7509            } else {
7510                // Bare SELECT: prescan to find the LOOP boundary.
7511                let mut depth: i32 = 0;
7512                let mut loop_pos: Option<usize> = None;
7513                let mut scan = self.pos;
7514                while scan < self.tokens.len() {
7515                    match self.tokens.get(scan) {
7516                        Some(Token::LParen) => depth += 1,
7517                        Some(Token::RParen) => depth -= 1,
7518                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7519                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7520                        {
7521                            loop_pos = Some(scan);
7522                            break;
7523                        }
7524                        _ => {}
7525                    }
7526                    scan += 1;
7527                }
7528                let loop_pos = loop_pos.ok_or_else(|| {
7529                    self.err(alloc::format!(
7530                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7531                    ))
7532                })?;
7533                // Swap the LOOP token with a synthetic Semicolon so
7534                // parse_select_stmt stops there, then restore afterward.
7535                let saved_loop = self.tokens[loop_pos].clone();
7536                self.tokens[loop_pos] = Token::Semicolon;
7537                let parse_result = self.parse_select_stmt();
7538                self.tokens[loop_pos] = saved_loop;
7539                let inner = parse_result?;
7540                let Statement::Select(q) = inner else {
7541                    return Err(self.err(alloc::format!(
7542                        "expected SELECT after FOR <var> IN, got {:?}",
7543                        self.peek()
7544                    )));
7545                };
7546                q
7547            };
7548            let loop_kw = self.expect_ident_like()?;
7549            if !loop_kw.eq_ignore_ascii_case("loop") {
7550                return Err(self.err(alloc::format!(
7551                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7552                )));
7553            }
7554            let body = self.parse_plpgsql_stmt_list_until_end()?;
7555            let end_kw = self.expect_ident_like()?;
7556            if !end_kw.eq_ignore_ascii_case("end") {
7557                return Err(self.err(alloc::format!(
7558                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7559                )));
7560            }
7561            let loop_kw2 = self.expect_ident_like()?;
7562            if !loop_kw2.eq_ignore_ascii_case("loop") {
7563                return Err(self.err(alloc::format!(
7564                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7565                )));
7566            }
7567            return Ok(PlPgSqlStmt::ForQuery {
7568                var,
7569                query: Box::new(query),
7570                body,
7571            });
7572        }
7573        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7574        // FOR is a reserved keyword token (Token::For).
7575        if matches!(self.peek(), Token::For)
7576            && matches!(
7577                self.tokens.get(self.pos + 1),
7578                Some(Token::Ident(_) | Token::QuotedIdent(_))
7579            )
7580            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7581        {
7582            self.advance(); // FOR
7583            let var = self.expect_ident_like()?;
7584            if !matches!(self.peek(), Token::In) {
7585                return Err(self.err(alloc::format!(
7586                    "expected IN after FOR <var>, got {:?}",
7587                    self.peek()
7588                )));
7589            }
7590            self.advance();
7591            let reverse = matches!(
7592                self.peek(),
7593                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7594            );
7595            if reverse {
7596                self.advance();
7597            }
7598            let start = self.parse_expr(0)?;
7599            if !matches!(self.peek(), Token::DotDot) {
7600                return Err(self.err(alloc::format!(
7601                    "expected '..' between FOR loop bounds, got {:?}",
7602                    self.peek()
7603                )));
7604            }
7605            self.advance();
7606            let end = self.parse_expr(0)?;
7607            let loop_kw = self.expect_ident_like()?;
7608            if !loop_kw.eq_ignore_ascii_case("loop") {
7609                return Err(self.err(alloc::format!(
7610                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7611                )));
7612            }
7613            let body = self.parse_plpgsql_stmt_list_until_end()?;
7614            let end_kw = self.expect_ident_like()?;
7615            if !end_kw.eq_ignore_ascii_case("end") {
7616                return Err(self.err(alloc::format!(
7617                    "expected END LOOP after FOR body, got {end_kw:?}"
7618                )));
7619            }
7620            let loop_kw2 = self.expect_ident_like()?;
7621            if !loop_kw2.eq_ignore_ascii_case("loop") {
7622                return Err(self.err(alloc::format!(
7623                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7624                )));
7625            }
7626            return Ok(PlPgSqlStmt::ForRange {
7627                var,
7628                start,
7629                end,
7630                reverse,
7631                body,
7632            });
7633        }
7634        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7635        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7636        {
7637            self.advance();
7638            let body = self.parse_plpgsql_stmt_list_until_end()?;
7639            let end_kw = self.expect_ident_like()?;
7640            if !end_kw.eq_ignore_ascii_case("end") {
7641                return Err(self.err(alloc::format!(
7642                    "expected END LOOP after LOOP body, got {end_kw:?}"
7643                )));
7644            }
7645            let loop_kw = self.expect_ident_like()?;
7646            if !loop_kw.eq_ignore_ascii_case("loop") {
7647                return Err(self.err(alloc::format!(
7648                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7649                )));
7650            }
7651            return Ok(PlPgSqlStmt::Loop { body });
7652        }
7653        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7654        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7655        {
7656            self.advance();
7657            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7658            {
7659                self.advance();
7660                Some(self.parse_expr(0)?)
7661            } else {
7662                None
7663            };
7664            return Ok(PlPgSqlStmt::Exit { when });
7665        }
7666        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7667        // already-parsed Statement or a runtime-computed SQL string.
7668        // The disambiguator vs the extended-query-protocol `EXECUTE
7669        // <stmt_name>` (which is a top-level Statement, not a
7670        // plpgsql line) is that inside a DO block / trigger body the
7671        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7672        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7673        {
7674            self.advance();
7675            let sql = self.parse_expr(0)?;
7676            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7677        }
7678        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7679        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7680        {
7681            self.advance();
7682            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7683            {
7684                self.advance();
7685                Some(self.parse_expr(0)?)
7686            } else {
7687                None
7688            };
7689            return Ok(PlPgSqlStmt::Continue { when });
7690        }
7691        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7692        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7693        {
7694            self.advance();
7695            let condition = self.parse_expr(0)?;
7696            let loop_kw = self.expect_ident_like()?;
7697            if !loop_kw.eq_ignore_ascii_case("loop") {
7698                return Err(self.err(alloc::format!(
7699                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7700                )));
7701            }
7702            let body = self.parse_plpgsql_stmt_list_until_end()?;
7703            // Expect END LOOP.
7704            let end_kw = self.expect_ident_like()?;
7705            if !end_kw.eq_ignore_ascii_case("end") {
7706                return Err(self.err(alloc::format!(
7707                    "expected END LOOP after WHILE body, got {end_kw:?}"
7708                )));
7709            }
7710            let loop_kw2 = self.expect_ident_like()?;
7711            if !loop_kw2.eq_ignore_ascii_case("loop") {
7712                return Err(self.err(alloc::format!(
7713                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7714                )));
7715            }
7716            return Ok(PlPgSqlStmt::While { condition, body });
7717        }
7718        // v7.12.6 — RAISE.
7719        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7720        {
7721            self.advance();
7722            return self.parse_plpgsql_raise();
7723        }
7724        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7725        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7726        {
7727            self.advance();
7728            let condition = self.parse_expr(0)?;
7729            let message = if matches!(self.peek(), Token::Comma) {
7730                self.advance();
7731                Some(self.parse_expr(0)?)
7732            } else {
7733                None
7734            };
7735            return Ok(PlPgSqlStmt::Assert { condition, message });
7736        }
7737        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7738        //   "PERFORM is equivalent to SELECT but discards the
7739        //    result." Side effects (function calls, RAISE inside
7740        //    SQL functions, etc.) still execute. We desugar to
7741        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7742        //    existing embedded-statement path handles execution +
7743        //    result-discard cleanly. The result is naturally
7744        //    discarded because EmbeddedSql doesn't propagate row
7745        //    sets back to the plpgsql interpreter.
7746        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7747        {
7748            self.advance();
7749            // Splice a synthetic Token::Select into the stream at
7750            // the current position so parse_select_stmt parses the
7751            // remainder as a normal SELECT body. Token-stream
7752            // surgery mirrors the try_parse_plpgsql_select_into
7753            // pattern used for SELECT … INTO desugaring.
7754            self.tokens.insert(self.pos, Token::Select);
7755            let select = self.parse_select_stmt()?;
7756            let Statement::Select(s) = select else {
7757                return Err(self.err(alloc::format!(
7758                    "expected SELECT body after PERFORM, got {:?}",
7759                    self.peek()
7760                )));
7761            };
7762            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7763        }
7764        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7765        // plpgsql-specific shape (mailrs round-10 migrate-042).
7766        // PG's SELECT INTO at top-level SQL would CREATE a new
7767        // table; inside plpgsql it ASSIGNS the query result to
7768        // a local variable. We detect the INTO at paren-depth
7769        // 0 between SELECT and the statement boundary; if
7770        // found, split the token stream into "pre-INTO
7771        // projection" + "var" + "post-INTO FROM/WHERE…" and
7772        // rebuild as a SelectInto with a regular SELECT body
7773        // (no INTO clause).
7774        if matches!(self.peek(), Token::Select)
7775            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7776        {
7777            return Ok(PlPgSqlStmt::SelectInto {
7778                var: var_name,
7779                body: Box::new(select_body),
7780            });
7781        }
7782        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7783        // SELECT can appear directly inside a trigger body; we
7784        // recurse into the regular Statement parser, which will
7785        // stop at the trailing `;` (which our caller then
7786        // consumes).
7787        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7788        // also embed ALTER / CREATE / DROP statements; route
7789        // those through the same parser so the DO body parses
7790        // cleanly.
7791        if matches!(self.peek(), Token::Insert)
7792            || matches!(self.peek(), Token::Select)
7793            || matches!(self.peek(), Token::Create)
7794            || matches!(self.peek(), Token::Drop)
7795            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7796                if s.eq_ignore_ascii_case("update")
7797                    || s.eq_ignore_ascii_case("delete")
7798                    || s.eq_ignore_ascii_case("alter"))
7799        {
7800            let stmt = self.parse_one_statement()?;
7801            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7802        }
7803        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7804        // followed by `:=` and an expression.
7805        let target = self.parse_plpgsql_assign_target()?;
7806        // PL/pgSQL assignment uses `:=`. The lexer represents
7807        // this as a colon followed by `=`; check both shapes.
7808        match self.peek() {
7809            Token::ColonEq => {
7810                self.advance();
7811            }
7812            Token::Colon => {
7813                self.advance();
7814                if !matches!(self.peek(), Token::Eq) {
7815                    return Err(self.err(alloc::format!(
7816                        "expected := after plpgsql assign target, got `:` then {:?}",
7817                        self.peek()
7818                    )));
7819                }
7820                self.advance();
7821            }
7822            other => {
7823                return Err(self.err(alloc::format!(
7824                    "expected := after plpgsql assign target, got {other:?}"
7825                )));
7826            }
7827        }
7828        let value = self.parse_expr(0)?;
7829        Ok(PlPgSqlStmt::Assign { target, value })
7830    }
7831
7832    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7833    /// [ELSE body] END IF`. `IF` keyword already consumed.
7834    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7835        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7836        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7837        loop {
7838            // <expr> THEN
7839            let cond = self.parse_expr(0)?;
7840            let then_kw = self.expect_ident_like()?;
7841            if !then_kw.eq_ignore_ascii_case("then") {
7842                return Err(self.err(alloc::format!(
7843                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7844                )));
7845            }
7846            let body = self.parse_plpgsql_stmt_list_until_end()?;
7847            branches.push((cond, body));
7848            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7849            match self.peek() {
7850                Token::Ident(s) | Token::QuotedIdent(s)
7851                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7852                {
7853                    self.advance();
7854                    continue;
7855                }
7856                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7857                    self.advance();
7858                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7859                    break;
7860                }
7861                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7862                    break;
7863                }
7864                other => {
7865                    return Err(self.err(alloc::format!(
7866                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7867                    )));
7868                }
7869            }
7870        }
7871        // Expect `END IF` (the END keyword is the one we're
7872        // looking at right now).
7873        let end_kw = self.expect_ident_like()?;
7874        if !end_kw.eq_ignore_ascii_case("end") {
7875            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7876        }
7877        let if_kw = self.expect_ident_like()?;
7878        if !if_kw.eq_ignore_ascii_case("if") {
7879            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7880        }
7881        Ok(PlPgSqlStmt::If {
7882            branches,
7883            else_branch,
7884        })
7885    }
7886
7887    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7888    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7889    /// is already consumed.
7890    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7891        let lvl_ident = self.expect_ident_like()?;
7892        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7893            "notice" => RaiseLevel::Notice,
7894            "warning" => RaiseLevel::Warning,
7895            "info" => RaiseLevel::Info,
7896            "log" => RaiseLevel::Log,
7897            "debug" => RaiseLevel::Debug,
7898            "exception" => RaiseLevel::Exception,
7899            other => {
7900                return Err(self.err(alloc::format!(
7901                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7902                )));
7903            }
7904        };
7905        // Message: required for v7.12.6. PG accepts a bare
7906        // RAISE-rethrow form (no message), reserved for future
7907        // RAISE-no-args support.
7908        let Token::String(msg) = self.peek() else {
7909            return Err(self.err(alloc::format!(
7910                "expected RAISE message string, got {:?}",
7911                self.peek()
7912            )));
7913        };
7914        let message = msg.clone();
7915        self.advance();
7916        // Optional comma-separated args (PG `%` format substitution).
7917        let mut args: Vec<Expr> = Vec::new();
7918        while matches!(self.peek(), Token::Comma) {
7919            self.advance();
7920            args.push(self.parse_expr(0)?);
7921        }
7922        Ok(PlPgSqlStmt::Raise {
7923            level,
7924            message,
7925            args,
7926        })
7927    }
7928
7929    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7930    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7931    /// migrate-042). Returns `(rebuilt_select_without_into,
7932    /// var_name)` when the pattern matches; `None` for
7933    /// regular SELECTs (those go through the embedded-SQL
7934    /// path). Token-stream surgery so the rebuilt SELECT
7935    /// parses through the regular `parse_select_stmt`.
7936    #[allow(clippy::too_many_lines)]
7937    fn try_parse_plpgsql_select_into(
7938        &mut self,
7939    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7940        // Scan forward from `self.pos + 1` (past Token::Select)
7941        // for Token::Into at paren-depth 0, stopping at the
7942        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7943        // end the plpgsql statement.
7944        let start = self.pos;
7945        let mut into_pos: Option<usize> = None;
7946        let mut depth: i32 = 0;
7947        let mut i = start + 1;
7948        while i < self.tokens.len() {
7949            match &self.tokens[i] {
7950                Token::LParen => depth += 1,
7951                Token::RParen => depth -= 1,
7952                Token::Semicolon if depth == 0 => break,
7953                Token::Ident(s)
7954                    if depth == 0
7955                        && (s.eq_ignore_ascii_case("end")
7956                            || s.eq_ignore_ascii_case("else")
7957                            || s.eq_ignore_ascii_case("elsif")) =>
7958                {
7959                    break;
7960                }
7961                Token::Into if depth == 0 => {
7962                    into_pos = Some(i);
7963                    break;
7964                }
7965                _ => {}
7966            }
7967            i += 1;
7968        }
7969        let Some(into_at) = into_pos else {
7970            return Ok(None);
7971        };
7972        // The token immediately after INTO must be the target
7973        // var ident; anything else (e.g. INSERT INTO table)
7974        // ruled out by the depth-0 check above. Capture it.
7975        let var = match self.tokens.get(into_at + 1) {
7976            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
7977            other => {
7978                return Err(self.err(alloc::format!(
7979                    "expected variable name after SELECT … INTO, got {other:?}"
7980                )));
7981            }
7982        };
7983        // Find the end of the plpgsql SELECT INTO statement —
7984        // same boundary rules as the depth-0 scan above.
7985        let mut end = into_at + 2;
7986        let mut depth2: i32 = 0;
7987        while end < self.tokens.len() {
7988            match &self.tokens[end] {
7989                Token::LParen => depth2 += 1,
7990                Token::RParen => depth2 -= 1,
7991                Token::Semicolon if depth2 == 0 => break,
7992                Token::Ident(s)
7993                    if depth2 == 0
7994                        && (s.eq_ignore_ascii_case("end")
7995                            || s.eq_ignore_ascii_case("else")
7996                            || s.eq_ignore_ascii_case("elsif")) =>
7997                {
7998                    break;
7999                }
8000                _ => {}
8001            }
8002            end += 1;
8003        }
8004        // Rebuild a token stream that represents the SELECT
8005        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
8006        // post-var tokens up to statement end]. Run the
8007        // regular `parse_select_stmt` against it.
8008        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
8009        for j in start..into_at {
8010            rebuilt.push(self.tokens[j].clone());
8011        }
8012        for j in (into_at + 2)..end {
8013            rebuilt.push(self.tokens[j].clone());
8014        }
8015        rebuilt.push(Token::Eof);
8016        let saved_pos = self.pos;
8017        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
8018        self.pos = 0;
8019        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
8020        if !matches!(self.peek(), Token::Select) {
8021            self.tokens = saved_tokens;
8022            self.pos = saved_pos;
8023            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
8024        }
8025        let sel = self.parse_select_stmt();
8026        self.tokens = saved_tokens;
8027        self.pos = end;
8028        let sel = sel?;
8029        let Statement::Select(body) = sel else {
8030            return Err(self.err(alloc::format!(
8031                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
8032            )));
8033        };
8034        Ok(Some((body, var)))
8035    }
8036
8037    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8038        // v7.16.1 — read the head token DIRECTLY rather than
8039        // via `expect_ident_like`. The v7.14.0 schema-qualifier
8040        // strip (`public.t` → `t`) inside `expect_ident_like`
8041        // greedily consumes any `ident . ident` pair, which
8042        // silently turned every `NEW.col := …` /
8043        // `OLD.col := …` plpgsql assignment into a Local("col")
8044        // assignment — the head "new"/"old" was eaten as if it
8045        // were a schema name and the Dot was consumed too, so
8046        // this function's own `peek() == Token::Dot` check
8047        // below never fired. Every BEFORE trigger that rewrote
8048        // a NEW cell was a silent no-op for two major releases
8049        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8050        // gate failures were investigated as v7.16.1 backlog.
8051        let head = match self.advance() {
8052            Token::Ident(s) | Token::QuotedIdent(s) => s,
8053            other => {
8054                return Err(self.err(alloc::format!(
8055                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8056                )));
8057            }
8058        };
8059        if matches!(self.peek(), Token::Dot) {
8060            self.advance();
8061            let col = self.expect_ident_like()?;
8062            if head.eq_ignore_ascii_case("new") {
8063                return Ok(AssignTarget::NewColumn(col));
8064            }
8065            if head.eq_ignore_ascii_case("old") {
8066                return Ok(AssignTarget::OldColumn(col));
8067            }
8068            return Err(self.err(alloc::format!(
8069                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8070                 got {head:?}.<col>"
8071            )));
8072        }
8073        Ok(AssignTarget::Local(head))
8074    }
8075
8076    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8077        // RETURN NEW / OLD / NULL — bare-ident forms.
8078        match self.peek() {
8079            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8080                self.advance();
8081                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8082            }
8083            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8084                self.advance();
8085                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8086            }
8087            Token::Null => {
8088                self.advance();
8089                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8090            }
8091            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8092            // per PL/pgSQL convention.
8093            Token::Semicolon => {
8094                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8095            }
8096            _ => {}
8097        }
8098        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8099        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8100        // caller-visible effect (blocks don't return sets), so we
8101        // desugar it identically to PERFORM: parse the SELECT (or
8102        // EXECUTE dynamic) as embedded SQL that runs for side
8103        // effects and discards the result. RETURN NEXT <expr>
8104        // (single-row accumulator) queues with v7.40 SETOF function
8105        // infrastructure.
8106        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8107        // and keep going.
8108        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8109        {
8110            self.advance();
8111            let e = self.parse_expr(0)?;
8112            return Ok(PlPgSqlStmt::ReturnNext(e));
8113        }
8114        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8115        {
8116            self.advance();
8117            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8118            // rows go to the set, like the static form. It used to desugar to a
8119            // bare ExecuteDynamic, whose result was DISCARDED.
8120            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8121            {
8122                self.advance();
8123                let sql = self.parse_expr(0)?;
8124                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8125            }
8126            // Bare RETURN QUERY <select>. If the current token is
8127            // not already SELECT (e.g., the user wrote `RETURN QUERY
8128            // <projection> FROM ...` in a shorthand — rare but PG
8129            // accepts a bare projection here), splice one in. Same
8130            // trick as PERFORM.
8131            if !matches!(self.peek(), Token::Select) {
8132                self.tokens.insert(self.pos, Token::Select);
8133            }
8134            let select = self.parse_select_stmt()?;
8135            let Statement::Select(s) = select else {
8136                return Err(self.err(alloc::format!(
8137                    "expected SELECT body after RETURN QUERY, got {:?}",
8138                    self.peek()
8139                )));
8140            };
8141            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8142            // to an embedded side-effect SELECT whose rows were DISCARDED, which
8143            // in a SETOF function is the entire answer thrown away.
8144            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8145        }
8146        // Fall through: parse a full expression.
8147        let e = self.parse_expr(0)?;
8148        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8149    }
8150
8151    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8152        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8153        // are ident-shaped (the parser keys off case-insensitive
8154        // match — same shape used by the top-level Update / Delete
8155        // dispatchers at parse_one_statement).
8156        if matches!(self.peek(), Token::Insert) {
8157            self.advance();
8158            return Ok(TriggerEvent::Insert);
8159        }
8160        match self.peek() {
8161            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8162                self.advance();
8163                Ok(TriggerEvent::Update)
8164            }
8165            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8166                self.advance();
8167                Ok(TriggerEvent::Delete)
8168            }
8169            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8170                self.advance();
8171                Ok(TriggerEvent::Truncate)
8172            }
8173            other => Err(self.err(alloc::format!(
8174                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8175            ))),
8176        }
8177    }
8178
8179    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8180    ///   - (no clause) → implicit `FOR ALL TABLES`
8181    ///   - `FOR ALL TABLES`
8182    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8183    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8184    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
8185    ///     REJECTS the bare plural (`invalid publication object list`,
8186    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
8187    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8188    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8189        let name = self.expect_ident_or_string()?;
8190        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8191        // shape so existing publications keep parsing identically.
8192        let scope = if matches!(self.peek(), Token::For) {
8193            self.advance();
8194            if matches!(self.peek(), Token::All) {
8195                self.advance();
8196                if !matches!(self.peek(), Token::Tables) {
8197                    return Err(self.err(format!(
8198                        "expected TABLES after FOR ALL, got {:?}",
8199                        self.peek()
8200                    )));
8201                }
8202                self.advance();
8203                if matches!(self.peek(), Token::Except) {
8204                    self.advance();
8205                    let tables = self.parse_publication_table_list()?;
8206                    PublicationScope::AllTablesExcept(tables)
8207                } else {
8208                    PublicationScope::AllTables
8209                }
8210            } else if matches!(self.peek(), Token::Table) {
8211                self.advance();
8212                let tables = self.parse_publication_table_list()?;
8213                PublicationScope::ForTables(tables)
8214            } else if matches!(self.peek(), Token::Tables) {
8215                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8216                // plural (`FOR TABLES t`) is REJECTED (`invalid
8217                // publication object list`); TABLES only pairs with
8218                // `IN SCHEMA`. The old arm accepted it on an
8219                // unverifiable "PG 19 accepts both" claim.
8220                self.advance();
8221                if !matches!(self.peek(), Token::In) {
8222                    return Err(self.err(alloc::string::String::from(
8223                        "invalid publication object list",
8224                    )));
8225                }
8226                self.advance();
8227                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8228                    return Err(self.err(format!(
8229                        "expected SCHEMA after FOR TABLES IN, got {:?}",
8230                        self.peek()
8231                    )));
8232                }
8233                self.advance();
8234                let schema = self.expect_ident_or_string()?;
8235                PublicationScope::TablesInSchema(schema)
8236            } else {
8237                return Err(self.err(format!(
8238                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8239                    self.peek()
8240                )));
8241            }
8242        } else {
8243            PublicationScope::AllTables
8244        };
8245        Ok(Statement::CreatePublication(CreatePublicationStatement {
8246            name,
8247            scope,
8248        }))
8249    }
8250
8251    /// v6.1.3 — Comma-separated identifier list for the publication
8252    /// FOR-clause. Requires at least one entry; empty list is a
8253    /// parse error (PG behaviour). Quoted idents are accepted; the
8254    /// names round-trip through `Display` as `quote_ident(name)`.
8255    ///
8256    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8257    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8258    /// pg_dump output. SPG's publication state today is per-table
8259    /// only (matching the pre-PG-15 surface); the col list + WHERE
8260    /// are parsed so dumps load through and the table name reaches
8261    /// `PublicationScope::ForTables`, but the filter is not enforced
8262    /// at publish time. Re-open when a customer dogfood gate
8263    /// requires per-row-filter or column-subset publish semantics
8264    /// (which gates on persistent slot state landing first, 21.12).
8265    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8266        let first = self.parse_publication_table_entry()?;
8267        let mut out = alloc::vec![first];
8268        while matches!(self.peek(), Token::Comma) {
8269            self.advance();
8270            out.push(self.parse_publication_table_entry()?);
8271        }
8272        Ok(out)
8273    }
8274
8275    /// One table entry inside a FOR TABLE clause:
8276    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8277    /// Returns just the table name; the column list + WHERE predicate
8278    /// are consumed and discarded per the parse-accept-discard
8279    /// commitment above.
8280    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8281        let name = self.expect_ident_like()?;
8282        // Optional column list — `(col, col, …)`.
8283        if matches!(self.peek(), Token::LParen) {
8284            self.advance();
8285            // Empty parens are a PG error too; require ≥ 1 column.
8286            let _ = self.expect_ident_like()?;
8287            while matches!(self.peek(), Token::Comma) {
8288                self.advance();
8289                let _ = self.expect_ident_like()?;
8290            }
8291            if !matches!(self.peek(), Token::RParen) {
8292                return Err(self.err(alloc::format!(
8293                    "expected ')' to close publication column list, got {:?}",
8294                    self.peek()
8295                )));
8296            }
8297            self.advance();
8298        }
8299        // Optional row filter — `WHERE (predicate)`.
8300        if matches!(self.peek(), Token::Where) {
8301            self.advance();
8302            if !matches!(self.peek(), Token::LParen) {
8303                return Err(self.err(alloc::format!(
8304                    "expected '(' after WHERE in publication row filter, got {:?}",
8305                    self.peek()
8306                )));
8307            }
8308            self.advance();
8309            let _ = self.parse_expr(0)?;
8310            if !matches!(self.peek(), Token::RParen) {
8311                return Err(self.err(alloc::format!(
8312                    "expected ')' to close publication WHERE filter, got {:?}",
8313                    self.peek()
8314                )));
8315            }
8316            self.advance();
8317        }
8318        Ok(name)
8319    }
8320
8321    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8322    ///                 CONNECTION '<conn>'
8323    ///                 PUBLICATION <pub> [, <pub> ...]`.
8324    ///
8325    /// The clause order is fixed (CONNECTION first, then
8326    /// PUBLICATION) to match PG. No WITH-options accepted in
8327    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8328    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8329        let name = self.expect_ident_or_string()?;
8330        if !matches!(self.peek(), Token::Connection) {
8331            return Err(self.err(format!(
8332                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8333                self.peek()
8334            )));
8335        }
8336        self.advance();
8337        let conn_str = self.expect_string_literal()?;
8338        if !matches!(self.peek(), Token::Publication) {
8339            return Err(self.err(format!(
8340                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8341                self.peek()
8342            )));
8343        }
8344        self.advance();
8345        // Reuse the publication FOR-list parser shape: at least one
8346        // identifier, comma-separated.
8347        let first = self.expect_ident_like()?;
8348        let mut publications = alloc::vec![first];
8349        while matches!(self.peek(), Token::Comma) {
8350            self.advance();
8351            publications.push(self.expect_ident_like()?);
8352        }
8353        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8354            name,
8355            conn_str,
8356            publications,
8357        }))
8358    }
8359
8360    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8361    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8362    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8363    /// that fit `u64`.
8364    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8365    /// qualifier is a *namespace* the app owns (`app.user_id`,
8366    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8367    /// to discard. So parse the raw segments here instead of
8368    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8369    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8370    /// a single segment and round-trip unchanged.
8371    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8372        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8373        loop {
8374            let seg = match self.advance() {
8375                Token::Ident(s) | Token::QuotedIdent(s) => s,
8376                other if unreserved_keyword_text(&other).is_some() => {
8377                    unreserved_keyword_text(&other).unwrap()
8378                }
8379                other => {
8380                    return Err(ParseError {
8381                        message: format!("expected parameter name, got {other:?}"),
8382                        token_pos: self.consumed_pos(),
8383                    });
8384                }
8385            };
8386            parts.push(seg);
8387            if matches!(self.peek(), Token::Dot) {
8388                self.advance();
8389                continue;
8390            }
8391            break;
8392        }
8393        Ok(parts.join(".").to_ascii_lowercase())
8394    }
8395
8396    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8397        Self::parse_set_value_inner(self)
8398    }
8399
8400    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8401        match self.advance() {
8402            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8403            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8404                Ok(crate::ast::SetValue::Default)
8405            }
8406            Token::Ident(s) | Token::QuotedIdent(s) => {
8407                let mut accum = s;
8408                while matches!(self.peek(), Token::Dot) {
8409                    self.advance();
8410                    let next = self.expect_ident_like()?;
8411                    accum.push('.');
8412                    accum.push_str(&next);
8413                }
8414                Ok(crate::ast::SetValue::Ident(accum))
8415            }
8416            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8417            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8418            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8419            // spellings that lex as keyword tokens, not idents:
8420            // `SET standard_conforming_strings = on` is in every
8421            // pg_dump preamble (`off` already lexes as an ident).
8422            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8423            // DEFAULT lexes as its keyword token, so the ident arm above
8424            // never saw it and the everyday reset form was a syntax error.
8425            Token::Default => Ok(crate::ast::SetValue::Default),
8426            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8427            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8428            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8429            // v7.14.0 — MySQL session/user variable RHS
8430            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8431            // Wrap as Ident so the SET handler can record it; the
8432            // engine treats `@VAR` / `@@VAR` values as opaque
8433            // strings.
8434            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8435            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8436            // is the common MySQL preamble shape. Allow a `+` or
8437            // `-` prefix on negative numerics for parity with PG
8438            // (some param defaults are negative).
8439            Token::Minus => match self.advance() {
8440                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8441                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8442                other => Err(self.err(format!(
8443                    "expected numeric after `-` in SET value, got {other:?}"
8444                ))),
8445            },
8446            other => Err(self.err(format!(
8447                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8448            ))),
8449        }
8450    }
8451
8452    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8453    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8454    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8455    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8456    /// present). Modes are comma-separated per PG; SPG also
8457    /// accepts space-separated for tolerance. READ ONLY / WRITE
8458    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8459    /// surface but not behaviorally honoured today).
8460    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8461    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8462    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8463    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8464    /// session default rather than forcing READ COMMITTED.
8465    fn parse_isolation_level_clauses(
8466        &mut self,
8467    ) -> Result<crate::ast::TransactionModes, ParseError> {
8468        let mut level = IsolationLevel::default();
8469        let mut have_level = false;
8470        // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8471        // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8472        let mut read_only: Option<bool> = None;
8473        loop {
8474            // ISOLATION LEVEL …
8475            let saw_isolation =
8476                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8477            if saw_isolation {
8478                self.advance(); // ISOLATION
8479                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8480                    return Err(self.err(alloc::format!(
8481                        "expected LEVEL after ISOLATION, got {:?}",
8482                        self.peek()
8483                    )));
8484                }
8485                self.advance(); // LEVEL
8486                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8487                let w1 = self
8488                    .expect_ident_like()
8489                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8490                let lc = w1.to_ascii_lowercase();
8491                level = match lc.as_str() {
8492                    "serializable" => IsolationLevel::Serializable,
8493                    "repeatable" => {
8494                        // Expect READ
8495                        let w2 = self
8496                            .expect_ident_like()
8497                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8498                        if !w2.eq_ignore_ascii_case("read") {
8499                            return Err(self.err(alloc::format!(
8500                                "expected READ after REPEATABLE, got {w2:?}"
8501                            )));
8502                        }
8503                        IsolationLevel::RepeatableRead
8504                    }
8505                    "read" => {
8506                        let w2 = self
8507                            .expect_ident_like()
8508                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8509                        match w2.to_ascii_lowercase().as_str() {
8510                            "committed" => IsolationLevel::ReadCommitted,
8511                            "uncommitted" => IsolationLevel::ReadUncommitted,
8512                            other => {
8513                                return Err(self.err(alloc::format!(
8514                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8515                                )));
8516                            }
8517                        }
8518                    }
8519                    other => {
8520                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8521                    }
8522                };
8523                have_level = true;
8524            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8525                // v7.39 — READ ONLY | READ WRITE. The comment here used to
8526                // read "parsed, not behaviorally honoured", and it was
8527                // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8528                // opened an ordinary read-write transaction and accepted
8529                // every write in it.
8530                self.advance();
8531                match self.peek().clone() {
8532                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8533                        self.advance();
8534                        read_only = Some(true);
8535                    }
8536                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8537                        self.advance();
8538                        read_only = Some(false);
8539                    }
8540                    other => {
8541                        return Err(self.err(alloc::format!(
8542                            "expected ONLY or WRITE after READ, got {other:?}"
8543                        )));
8544                    }
8545                }
8546            } else if matches!(self.peek(), Token::Not) {
8547                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8548                self.advance();
8549                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8550                    return Err(self.err(alloc::format!(
8551                        "expected DEFERRABLE after NOT, got {:?}",
8552                        self.peek()
8553                    )));
8554                }
8555                self.advance();
8556            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8557            {
8558                self.advance();
8559            } else {
8560                break;
8561            }
8562            // Optional comma between modes.
8563            if matches!(self.peek(), Token::Comma) {
8564                self.advance();
8565            }
8566        }
8567        Ok(crate::ast::TransactionModes {
8568            isolation: have_level.then_some(level),
8569            read_only,
8570        })
8571    }
8572
8573    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8574        // FOR is a v6.1.2-reserved keyword (Token::For). The
8575        // other two are bare idents — they've never needed lexer
8576        // support and we keep it that way.
8577        if !matches!(self.peek(), Token::For) {
8578            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8579        }
8580        self.advance();
8581        self.expect_keyword_ident("wal")?;
8582        self.expect_keyword_ident("position")?;
8583        let pos = self.expect_u64_literal()?;
8584        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8585        {
8586            self.advance();
8587            self.expect_keyword_ident("timeout")?;
8588            Some(self.expect_u64_literal()?)
8589        } else {
8590            None
8591        };
8592        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8593    }
8594
8595    /// v6.1.7 helper — consume a `Token::Integer` and check it
8596    /// fits `u64`. WAL positions and millisecond timeouts are
8597    /// non-negative.
8598    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8599        match self.advance() {
8600            Token::Integer(n) if n >= 0 => Ok(n as u64),
8601            Token::Integer(n) => Err(ParseError {
8602                message: format!("expected non-negative integer, got {n}"),
8603                token_pos: self.consumed_pos(),
8604            }),
8605            other => Err(ParseError {
8606                message: format!("expected integer literal, got {other:?}"),
8607                token_pos: self.consumed_pos(),
8608            }),
8609        }
8610    }
8611
8612    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8613    /// ROLE '<role>' (defaults to readonly). All string slots accept
8614    /// either a quoted ident or a quoted string literal.
8615    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8616    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8617    ///
8618    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8619    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8620    /// wire role) still parses — it is a different axis from the PG attributes.
8621    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8622    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8623    /// or RESET, so the plain attribute forms keep their old path.
8624    fn peeks_db_role_setting(&self) -> bool {
8625        let mut i = self.pos + 1; // past the object's name
8626        let word = |p: usize| -> Option<String> {
8627            match self.tokens.get(p) {
8628                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8629                Some(Token::In) => Some(String::from("in")),
8630                _ => None,
8631            }
8632        };
8633        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8634            i += 3; // IN DATABASE <name>
8635        }
8636        matches!(word(i).as_deref(), Some("set" | "reset"))
8637    }
8638
8639    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8640        use crate::ast::SetDbRoleSettingStatement;
8641        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8642        // identifier, so the ordinary name reader refuses it. Same trap
8643        // as TABLE / INDEX / FULL / DEFAULT before it.
8644        let name = if matches!(self.peek(), Token::All) {
8645            self.advance();
8646            String::from("all")
8647        } else {
8648            self.expect_ident_or_string()?
8649        };
8650        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8651        let all = name.eq_ignore_ascii_case("all");
8652        let (mut database, mut role) = if is_database {
8653            (Some(name), None)
8654        } else if all {
8655            (None, None)
8656        } else {
8657            (None, Some(name))
8658        };
8659        if matches!(self.peek(), Token::In) {
8660            self.advance();
8661            self.advance(); // DATABASE
8662            database = Some(self.expect_ident_or_string()?);
8663        }
8664        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8665        self.advance(); // SET | RESET
8666        if resetting && matches!(self.peek(), Token::All) {
8667            self.advance();
8668            self.consume_until_statement_boundary();
8669            return Ok(Statement::SetDbRoleSetting(Box::new(
8670                SetDbRoleSettingStatement {
8671                    database,
8672                    role,
8673                    param: None,
8674                    value: None,
8675                },
8676            )));
8677        }
8678        let param = self.expect_ident_like()?;
8679        let value = if resetting {
8680            None
8681        } else {
8682            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8683            // KEYWORD, so the ident-only check missed it and consumed
8684            // the word itself as the value — the same trap as ALL, one
8685            // clause over.
8686            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8687                self.advance();
8688            }
8689            Some(self.take_guc_value())
8690        };
8691        self.consume_until_statement_boundary();
8692        Ok(Statement::SetDbRoleSetting(Box::new(
8693            SetDbRoleSettingStatement {
8694                database,
8695                role,
8696                param: Some(param),
8697                value,
8698            },
8699        )))
8700    }
8701
8702    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8703    /// a quoted literal loses its quotes, a bare word or number does not.
8704    fn take_guc_value(&mut self) -> String {
8705        match self.advance() {
8706            Token::String(s) => s,
8707            Token::Integer(n) => format!("{n}"),
8708            Token::Float(f) => format!("{f}"),
8709            Token::Ident(s) | Token::QuotedIdent(s) => s,
8710            other => format!("{other:?}"),
8711        }
8712    }
8713
8714    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8715        let name = self.expect_ident_or_string()?;
8716        if self.peek_keyword_ident("with") {
8717            self.advance();
8718        }
8719        let mut password = String::new();
8720        let mut role = String::new();
8721        let mut login: Option<bool> = None;
8722        let mut inherit: Option<bool> = None;
8723        let mut superuser: Option<bool> = None;
8724        // Not a `while let`: the pattern would borrow `self` across the
8725        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8726        #[allow(clippy::while_let_loop)]
8727        loop {
8728            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8729                break;
8730            };
8731            match w.to_ascii_lowercase().as_str() {
8732                "password" => {
8733                    self.advance();
8734                    password = self.expect_string_literal()?;
8735                }
8736                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8737                // is the same slot.
8738                "encrypted" => {
8739                    self.advance();
8740                    self.expect_keyword_ident("password")?;
8741                    password = self.expect_string_literal()?;
8742                }
8743                "login" => {
8744                    self.advance();
8745                    login = Some(true);
8746                }
8747                "nologin" => {
8748                    self.advance();
8749                    login = Some(false);
8750                }
8751                "inherit" => {
8752                    self.advance();
8753                    inherit = Some(true);
8754                }
8755                "noinherit" => {
8756                    self.advance();
8757                    inherit = Some(false);
8758                }
8759                "superuser" => {
8760                    self.advance();
8761                    superuser = Some(true);
8762                }
8763                "nosuperuser" => {
8764                    self.advance();
8765                    superuser = Some(false);
8766                }
8767                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8768                "role" => {
8769                    self.advance();
8770                    role = self.expect_string_literal()?;
8771                }
8772                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8773                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8774                // accepted and ignored so a pg_dump role block restores. They
8775                // gate capabilities SPG does not have.
8776                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8777                | "noreplication" | "bypassrls" | "nobypassrls" => {
8778                    self.advance();
8779                }
8780                "connection" => {
8781                    self.advance();
8782                    self.expect_keyword_ident("limit")?;
8783                    self.advance(); // the number
8784                }
8785                "valid" => {
8786                    self.advance();
8787                    self.expect_keyword_ident("until")?;
8788                    self.expect_string_literal()?;
8789                }
8790                _ => break,
8791            }
8792        }
8793        if role.is_empty() {
8794            role = "readonly".to_string();
8795        }
8796        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8797            name,
8798            password,
8799            role,
8800            login,
8801            inherit,
8802            superuser,
8803            is_user,
8804        }))
8805    }
8806
8807    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8808    /// consumed the USING / WITH CHECK keyword.
8809    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8810        if !matches!(self.peek(), Token::LParen) {
8811            return Err(self.err(alloc::format!(
8812                "expected '(' after {clause}, got {:?}",
8813                self.peek()
8814            )));
8815        }
8816        self.advance();
8817        let e = self.parse_expr(0)?;
8818        if !matches!(self.peek(), Token::RParen) {
8819            return Err(self.err(alloc::format!(
8820                "expected ')' to close {clause}, got {:?}",
8821                self.peek()
8822            )));
8823        }
8824        self.advance();
8825        Ok(e)
8826    }
8827
8828    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8829    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8830        let mut roles = Vec::new();
8831        loop {
8832            roles.push(self.expect_ident_like()?);
8833            if matches!(self.peek(), Token::Comma) {
8834                self.advance();
8835            } else {
8836                break;
8837            }
8838        }
8839        Ok(roles)
8840    }
8841
8842    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8843    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8844    /// `CREATE POLICY`.
8845    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8846        use crate::ast::PolicyCmd;
8847        let name = self.expect_ident_like()?;
8848        if !matches!(self.peek(), Token::On) {
8849            return Err(self.err(alloc::format!(
8850                "expected ON after CREATE POLICY name, got {:?}",
8851                self.peek()
8852            )));
8853        }
8854        self.advance();
8855        let table = self.expect_ident_like()?;
8856
8857        let mut permissive = true;
8858        if matches!(self.peek(), Token::As) {
8859            self.advance();
8860            let w = self.expect_ident_like()?;
8861            permissive = if w.eq_ignore_ascii_case("permissive") {
8862                true
8863            } else if w.eq_ignore_ascii_case("restrictive") {
8864                false
8865            } else {
8866                return Err(self.err(alloc::format!(
8867                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8868                )));
8869            };
8870        }
8871
8872        let mut cmd = PolicyCmd::All;
8873        if matches!(self.peek(), Token::For) {
8874            self.advance();
8875            cmd = self.parse_policy_cmd()?;
8876        }
8877
8878        let mut roles = Vec::new();
8879        if matches!(self.peek(), Token::To) {
8880            self.advance();
8881            roles = self.parse_policy_roles()?;
8882        }
8883
8884        let mut using = None;
8885        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8886        {
8887            self.advance();
8888            using = Some(self.parse_paren_expr("USING")?);
8889        }
8890
8891        let mut with_check = None;
8892        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8893        {
8894            self.advance();
8895            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8896            {
8897                return Err(self.err(alloc::format!(
8898                    "expected CHECK after WITH, got {:?}",
8899                    self.peek()
8900                )));
8901            }
8902            self.advance();
8903            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8904        }
8905
8906        // Clause-per-command matrix (PG wording).
8907        match cmd {
8908            PolicyCmd::Insert => {
8909                if using.is_some() {
8910                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8911                }
8912            }
8913            PolicyCmd::Select | PolicyCmd::Delete => {
8914                if with_check.is_some() {
8915                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8916                }
8917            }
8918            PolicyCmd::Update | PolicyCmd::All => {}
8919        }
8920
8921        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8922            name,
8923            table,
8924            permissive,
8925            cmd,
8926            roles,
8927            using,
8928            with_check,
8929        }))
8930    }
8931
8932    /// v7.39 (RLS) — the command word after `FOR`.
8933    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8934        use crate::ast::PolicyCmd;
8935        match self.peek().clone() {
8936            Token::All => {
8937                self.advance();
8938                Ok(PolicyCmd::All)
8939            }
8940            Token::Select => {
8941                self.advance();
8942                Ok(PolicyCmd::Select)
8943            }
8944            Token::Insert => {
8945                self.advance();
8946                Ok(PolicyCmd::Insert)
8947            }
8948            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8949                self.advance();
8950                Ok(PolicyCmd::Update)
8951            }
8952            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8953                self.advance();
8954                Ok(PolicyCmd::Delete)
8955            }
8956            other => Err(self.err(alloc::format!(
8957                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8958            ))),
8959        }
8960    }
8961
8962    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
8963    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
8964    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8965        let name = self.expect_ident_like()?;
8966        if !matches!(self.peek(), Token::On) {
8967            return Err(self.err(alloc::format!(
8968                "expected ON after ALTER POLICY name, got {:?}",
8969                self.peek()
8970            )));
8971        }
8972        self.advance();
8973        let table = self.expect_ident_like()?;
8974
8975        // RENAME TO new
8976        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
8977        {
8978            self.advance();
8979            if !matches!(self.peek(), Token::To) {
8980                return Err(self.err(alloc::format!(
8981                    "expected TO after RENAME, got {:?}",
8982                    self.peek()
8983                )));
8984            }
8985            self.advance();
8986            let new = self.expect_ident_like()?;
8987            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
8988                name,
8989                table,
8990                rename_to: Some(new),
8991                roles: None,
8992                using: None,
8993                with_check: None,
8994            }));
8995        }
8996
8997        let mut roles = None;
8998        if matches!(self.peek(), Token::To) {
8999            self.advance();
9000            roles = Some(self.parse_policy_roles()?);
9001        }
9002        let mut using = None;
9003        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9004        {
9005            self.advance();
9006            using = Some(self.parse_paren_expr("USING")?);
9007        }
9008        let mut with_check = None;
9009        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9010        {
9011            self.advance();
9012            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9013            {
9014                return Err(self.err(alloc::format!(
9015                    "expected CHECK after WITH, got {:?}",
9016                    self.peek()
9017                )));
9018            }
9019            self.advance();
9020            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9021        }
9022        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9023            name,
9024            table,
9025            rename_to: None,
9026            roles,
9027            using,
9028            with_check,
9029        }))
9030    }
9031
9032    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
9033    /// `DROP POLICY`.
9034    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9035        let if_exists = self.consume_if_exists();
9036        let name = self.expect_ident_like()?;
9037        if !matches!(self.peek(), Token::On) {
9038            return Err(self.err(alloc::format!(
9039                "expected ON after DROP POLICY name, got {:?}",
9040                self.peek()
9041            )));
9042        }
9043        self.advance();
9044        let table = self.expect_ident_like()?;
9045        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9046            name,
9047            table,
9048            if_exists,
9049        }))
9050    }
9051}
9052fn wrap_from_leaves(
9053    e: &mut Expr,
9054    names: &[String],
9055    make: &dyn Fn(Expr) -> Expr,
9056    refs: &dyn Fn(&Expr) -> bool,
9057) {
9058    if let Expr::Column(c) = e {
9059        if c.qualifier
9060            .as_deref()
9061            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9062        {
9063            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9064            *e = make(taken);
9065        }
9066        return;
9067    }
9068    match e {
9069        Expr::Binary { lhs, rhs, .. } => {
9070            wrap_from_leaves(lhs, names, make, refs);
9071            wrap_from_leaves(rhs, names, make, refs);
9072        }
9073        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9074            wrap_from_leaves(expr, names, make, refs)
9075        }
9076        Expr::FunctionCall { args, .. } => {
9077            for a in args.iter_mut() {
9078                wrap_from_leaves(a, names, make, refs);
9079            }
9080        }
9081        Expr::Case {
9082            operand,
9083            branches,
9084            else_branch,
9085        } => {
9086            if let Some(o) = operand.as_deref_mut() {
9087                wrap_from_leaves(o, names, make, refs);
9088            }
9089            for (w, t) in branches.iter_mut() {
9090                wrap_from_leaves(w, names, make, refs);
9091                wrap_from_leaves(t, names, make, refs);
9092            }
9093            if let Some(el) = else_branch.as_deref_mut() {
9094                wrap_from_leaves(el, names, make, refs);
9095            }
9096        }
9097        // Compound variants the walk doesn't decompose: keep the
9098        // pre-D.30 behavior — wrap the whole sub-expr if it touches
9099        // a source table, so nothing regresses.
9100        other => {
9101            if refs(other) {
9102                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9103                *other = make(taken);
9104            }
9105        }
9106    }
9107}
9108
9109/// v7.39 (round 241) — does this expression reference any of the FROM /
9110/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9111/// lowerings)?
9112fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9113    match e {
9114        Expr::Column(c) => c
9115            .qualifier
9116            .as_deref()
9117            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9118        Expr::Binary { lhs, rhs, .. } => {
9119            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9120        }
9121        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9122        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9123        Expr::Case {
9124            operand,
9125            branches,
9126            else_branch,
9127        } => {
9128            operand
9129                .as_deref()
9130                .is_some_and(|o| expr_refs_tables(o, names))
9131                || branches
9132                    .iter()
9133                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9134                || else_branch
9135                    .as_deref()
9136                    .is_some_and(|el| expr_refs_tables(el, names))
9137        }
9138        _ => false,
9139    }
9140}
9141
9142impl Parser {
9143    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9144    /// Caller already consumed the leading `UPDATE` ident.
9145    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9146    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9147    /// after the target name has been read. `JOIN` is a reserved token;
9148    /// the qualifiers are bare idents.
9149    fn peek_is_update_join_start(&self) -> bool {
9150        match self.peek() {
9151            // JOIN and its qualifiers are reserved lexer tokens (the grammar
9152            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9153            Token::Join
9154            | Token::Inner
9155            | Token::Left
9156            | Token::Right
9157            | Token::Cross
9158            | Token::Full => true,
9159            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9160            Token::Ident(s) | Token::QuotedIdent(s) => {
9161                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9162            }
9163            _ => false,
9164        }
9165    }
9166
9167    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9168    /// USER-variable assignment. Its own per-session namespace, an arbitrary
9169    /// expression on the right, and `:=` as a second spelling of `=`.
9170    ///
9171    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9172    /// from sits on the nesting recursion chain (a CTE body, a subquery),
9173    /// and holding this loop's `Vec` + `String` locals there overflowed the
9174    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9175    #[inline(never)]
9176    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9177        let mut assigns: Vec<(String, Expr)> = Vec::new();
9178        let mut settings: Vec<(String, Expr)> = Vec::new();
9179        loop {
9180            // v7.39 (round 554) — a plain NAME here is a session
9181            // setting, not a user variable. mysqldump writes the two in
9182            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9183            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9184            // changes it — and this refused the mixture outright, so no
9185            // dump could be restored past its preamble.
9186            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9187                self.advance();
9188                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9189                    return Err(self.err(alloc::format!(
9190                        "expected `=` after {name}, got {:?}",
9191                        self.peek()
9192                    )));
9193                }
9194                self.advance();
9195                let value = self.parse_expr(0)?;
9196                settings.push((name.to_ascii_lowercase(), value));
9197                if matches!(self.peek(), Token::Comma) {
9198                    self.advance();
9199                    continue;
9200                }
9201                break;
9202            }
9203            let Token::SessionVar(raw) = self.peek().clone() else {
9204                return Err(self.err(alloc::format!(
9205                    "expected a user variable after SET, got {:?}",
9206                    self.peek()
9207                )));
9208            };
9209            if raw.starts_with("@@") {
9210                return Err(self.err(alloc::string::String::from(
9211                    "cannot mix `@@` settings with `@` user variables in one SET",
9212                )));
9213            }
9214            self.advance();
9215            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9216                return Err(self.err(alloc::format!(
9217                    "expected `=` or `:=` after {raw}, got {:?}",
9218                    self.peek()
9219                )));
9220            }
9221            self.advance();
9222            let value = self.parse_expr(0)?;
9223            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9224            if matches!(self.peek(), Token::Comma) {
9225                self.advance();
9226                continue;
9227            }
9228            break;
9229        }
9230        Ok(Statement::SetUserVars(assigns, settings))
9231    }
9232
9233    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9234        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9235        // NAMED `only` until now, which failed on `relation "only" does
9236        // not exist`. The lookahead is what keeps a table actually
9237        // called `only` working: the keyword is only a keyword when a
9238        // TABLE NAME follows it — and `SET` arrives as an identifier
9239        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9240        // for the table and die on the `=`. Measured by the pin.
9241        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9242            if s.eq_ignore_ascii_case("only"))
9243            && matches!(
9244                self.tokens.get(self.pos + 1),
9245                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9246            );
9247        if only {
9248            self.advance();
9249        }
9250        let table = self.expect_ident_like()?;
9251        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9252        // bare spelling; a bare identifier that is the SET keyword itself
9253        // is the clause, not an alias.
9254        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9255        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9256        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9257        // following JOIN a syntax error.
9258        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9259        let alias = if matches!(self.peek(), Token::As) {
9260            self.advance();
9261            Some(self.expect_ident_like()?)
9262        } else {
9263            match self.peek() {
9264                Token::Ident(s) | Token::QuotedIdent(s)
9265                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
9266                {
9267                    let a = s.clone();
9268                    self.advance();
9269                    Some(a)
9270                }
9271                _ => None,
9272            }
9273        };
9274        // v7.39 (round 420) — MySQL's multi-table UPDATE:
9275        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
9276        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
9277        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9278        // The FIRST table is the mutation target and the rest are sources —
9279        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9280        // SPG already lowers onto correlated subqueries. So rewind, let
9281        // `parse_from_clause` read the whole list (it handles aliases, comma
9282        // lists, and every JOIN form), then peel the target off the front.
9283        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9284            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9285        {
9286            // NOTE: `advance()` destroys the tokens it returns
9287            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9288            // is NOT possible — the tail is read forward, once, through the
9289            // same grammar `parse_from_clause` uses after its primary.
9290            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9291            let mut joins = self.parse_from_joins(&target_qual)?;
9292            if joins.is_empty() {
9293                return Err(self.err(alloc::string::String::from(
9294                    "multi-table UPDATE needs at least one source table",
9295                )));
9296            }
9297            let head = joins.remove(0);
9298            // A LEFT join keeps every target row (the unmatched ones see NULL
9299            // on the source side), so it must NOT get the EXISTS row filter
9300            // the inner / comma forms use.
9301            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9302            let src = FromClause {
9303                primary: head.table,
9304                joins,
9305            };
9306            (Some(src), head.on, outer)
9307        } else {
9308            (None, None, false)
9309        };
9310        self.expect_keyword_ident("set")?;
9311        let mut assignments = Vec::new();
9312        loop {
9313            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9314            // …)` — the parenthesized multi-assignment. Expressions
9315            // assign positionally; a subquery RHS clones per column
9316            // keeping only the Nth projection item.
9317            if matches!(self.peek(), Token::LParen) {
9318                self.advance();
9319                let mut cols = alloc::vec![self.expect_ident_like()?];
9320                while matches!(self.peek(), Token::Comma) {
9321                    self.advance();
9322                    cols.push(self.expect_ident_like()?);
9323                }
9324                if !matches!(self.peek(), Token::RParen) {
9325                    return Err(self.err(format!(
9326                        "expected ')' after SET column list, got {:?}",
9327                        self.peek()
9328                    )));
9329                }
9330                self.advance();
9331                if !matches!(self.peek(), Token::Eq) {
9332                    return Err(self.err(format!(
9333                        "expected `=` after SET column list, got {:?}",
9334                        self.peek()
9335                    )));
9336                }
9337                self.advance();
9338                if !matches!(self.peek(), Token::LParen) {
9339                    return Err(self.err(format!(
9340                        "expected '(' after SET (…) =, got {:?}",
9341                        self.peek()
9342                    )));
9343                }
9344                self.advance();
9345                if matches!(self.peek(), Token::Select) {
9346                    let inner = match self.parse_select_stmt()? {
9347                        Statement::Select(s) => s,
9348                        other => {
9349                            return Err(self.err(alloc::format!(
9350                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9351                            )));
9352                        }
9353                    };
9354                    if !matches!(self.peek(), Token::RParen) {
9355                        return Err(self.err(format!(
9356                            "expected ')' after SET subquery, got {:?}",
9357                            self.peek()
9358                        )));
9359                    }
9360                    self.advance();
9361                    if inner.items.len() != cols.len() {
9362                        return Err(self.err(alloc::format!(
9363                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9364                            cols.len(),
9365                            inner.items.len()
9366                        )));
9367                    }
9368                    for (i, col) in cols.into_iter().enumerate() {
9369                        let mut sub = inner.clone();
9370                        sub.items = alloc::vec![sub.items[i].clone()];
9371                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9372                    }
9373                } else {
9374                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9375                    while matches!(self.peek(), Token::Comma) {
9376                        self.advance();
9377                        exprs.push(self.parse_expr(0)?);
9378                    }
9379                    if !matches!(self.peek(), Token::RParen) {
9380                        return Err(self.err(format!(
9381                            "expected ')' after SET row values, got {:?}",
9382                            self.peek()
9383                        )));
9384                    }
9385                    self.advance();
9386                    if exprs.len() != cols.len() {
9387                        return Err(self.err(alloc::format!(
9388                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9389                            cols.len(),
9390                            exprs.len()
9391                        )));
9392                    }
9393                    for (col, e) in cols.into_iter().zip(exprs) {
9394                        assignments.push((col, e));
9395                    }
9396                }
9397                if matches!(self.peek(), Token::Comma) {
9398                    self.advance();
9399                    continue;
9400                }
9401                break;
9402            }
9403            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9404            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9405            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9406            // `public.` dump qualifiers), so the qualifier has to be read off
9407            // the token stream first — otherwise `SET b.v = 888` would write
9408            // to the TARGET table's `v` while naming a source table, a
9409            // silent-wrong. A qualifier naming a SOURCE table means a
9410            // multi-TARGET update — mutating two tables in one statement —
9411            // which SPG does not model, so it is refused loudly.
9412            let set_qual: Option<String> = if mysql_from.is_some()
9413                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9414            {
9415                match self.peek() {
9416                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9417                    _ => None,
9418                }
9419            } else {
9420                None
9421            };
9422            let col = self.expect_ident_like()?;
9423            if let Some(q) = set_qual {
9424                let names_target = q.eq_ignore_ascii_case(&table)
9425                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9426                if !names_target {
9427                    return Err(self.err(alloc::format!(
9428                        "multi-table UPDATE can only assign to its first table \
9429                         ({table}); `{q}.{col}` targets another table"
9430                    )));
9431                }
9432            }
9433            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9434            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9435            // `__column_default` marker lowering just below). PG assigns to the
9436            // i-th (1-based) element, NULL-padding when i exceeds the length.
9437            if matches!(self.peek(), Token::LBracket) {
9438                self.advance();
9439                let index = self.parse_expr(0)?;
9440                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9441                // (and the open `arr[lo:]`), lowered to
9442                // `__array_assign_slice`. Only the single-subscript form
9443                // parsed before, so a slice assignment was a syntax error.
9444                let mut slice_hi: Option<Option<Expr>> = None;
9445                if matches!(self.peek(), Token::Colon) {
9446                    self.advance();
9447                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9448                        None
9449                    } else {
9450                        Some(self.parse_expr(0)?)
9451                    });
9452                }
9453                if !matches!(self.peek(), Token::RBracket) {
9454                    return Err(self.err(format!(
9455                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9456                        self.peek()
9457                    )));
9458                }
9459                self.advance();
9460                if !matches!(self.peek(), Token::Eq) {
9461                    return Err(self.err(format!(
9462                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9463                        self.peek()
9464                    )));
9465                }
9466                self.advance();
9467                let value = self.parse_expr(0)?;
9468                // PG merges several subscript writes to the same column into one
9469                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9470                // assignment to `col` rather than each overwriting the original.
9471                let existing = assignments.iter().position(|(c, _)| c == &col);
9472                let base = match existing {
9473                    Some(i) => assignments[i].1.clone(),
9474                    None => Expr::Column(ColumnName {
9475                        qualifier: None,
9476                        name: col.clone(),
9477                    }),
9478                };
9479                let assigned = match slice_hi {
9480                    None => Expr::FunctionCall {
9481                        name: "__array_assign".to_string(),
9482                        args: alloc::vec![base, index, value],
9483                    },
9484                    Some(hi) => Expr::FunctionCall {
9485                        name: "__array_assign_slice".to_string(),
9486                        args: alloc::vec![
9487                            base,
9488                            index,
9489                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9490                            value,
9491                        ],
9492                    },
9493                };
9494                match existing {
9495                    Some(i) => assignments[i].1 = assigned,
9496                    None => assignments.push((col, assigned)),
9497                }
9498                if matches!(self.peek(), Token::Comma) {
9499                    self.advance();
9500                    continue;
9501                }
9502                break;
9503            }
9504            if !matches!(self.peek(), Token::Eq) {
9505                return Err(self.err(format!(
9506                    "expected `=` after column name in UPDATE SET, got {:?}",
9507                    self.peek()
9508                )));
9509            }
9510            self.advance();
9511            // `SET col = DEFAULT` — the column's declared default;
9512            // rides out as a marker call the update executor
9513            // resolves against the schema.
9514            let value = if matches!(self.peek(), Token::Default) {
9515                self.advance();
9516                Expr::FunctionCall {
9517                    name: "__column_default".to_string(),
9518                    args: Vec::new(),
9519                }
9520            } else {
9521                self.parse_expr(0)?
9522            };
9523            assignments.push((col, value));
9524            if matches!(self.peek(), Token::Comma) {
9525                self.advance();
9526                continue;
9527            }
9528            break;
9529        }
9530        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9531        // update. Lowers onto the correlated-subquery machinery:
9532        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9533        // and each assignment that references a FROM-list table
9534        // wraps into a correlated scalar subquery
9535        // (SELECT expr FROM src WHERE cond). Equivalent for the
9536        // unique-join shape (the overwhelmingly common one); a
9537        // multi-match, which PG resolves by arbitrary pick,
9538        // surfaces as a scalar-subquery cardinality error instead
9539        // of a silent arbitrary result.
9540        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9541        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9542        // the SAME lowering below. Both spellings together is not legal in
9543        // either dialect.
9544        let from_clause = if let Some(fc) = mysql_from {
9545            if matches!(self.peek(), Token::From) {
9546                return Err(self.err(alloc::string::String::from(
9547                    "multi-table UPDATE already names its sources; drop the FROM clause",
9548                )));
9549            }
9550            Some(fc)
9551        } else if matches!(self.peek(), Token::From) {
9552            self.advance();
9553            Some(self.parse_from_clause()?)
9554        } else {
9555            None
9556        };
9557        let where_ = if matches!(self.peek(), Token::Where) {
9558            self.advance();
9559            Some(self.parse_expr(0)?)
9560        } else {
9561            None
9562        };
9563        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9564        // and the TARGET-row filter are NOT the same predicate once a LEFT
9565        // join is involved:
9566        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9567        //     one conjunction, and the whole thing filters target rows via
9568        //     EXISTS.
9569        //   * LEFT join: only the ON predicate belongs inside the source
9570        //     subquery. The WHERE still filters TARGET rows (with source
9571        //     columns read through the correlated subquery, which yields NULL
9572        //     for an unmatched row — exactly LEFT-join semantics).
9573        // Round 420 folded ON into WHERE unconditionally and then dropped the
9574        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9575        // WHERE a.id > 1` updated EVERY row.
9576        let sub_where = match (mysql_on.clone(), where_.clone()) {
9577            _ if mysql_outer => mysql_on.clone(),
9578            (Some(on), Some(w)) => Some(Expr::Binary {
9579                lhs: Box::new(on),
9580                op: crate::ast::BinOp::And,
9581                rhs: Box::new(w),
9582            }),
9583            (Some(on), None) => Some(on),
9584            (None, w) => w,
9585        };
9586        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9587        // has no such clause on UPDATE, so this is accepted only under the
9588        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9589        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9590        let mut returning = self.parse_optional_returning()?;
9591        // v7.39 (round 533) — kept for the engine, which can resolve the
9592        // UNQUALIFIED leaves this lowering has to leave alone.
9593        let from_sources = from_clause.as_ref().map(|fc| {
9594            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9595                from: fc.clone(),
9596                sub_where: sub_where.clone(),
9597            })
9598        });
9599        let (assignments, where_) = if let Some(fc) = from_clause {
9600            let names: Vec<String> = core::iter::once(&fc.primary)
9601                .chain(fc.joins.iter().map(|j| &j.table))
9602                .flat_map(|t| {
9603                    t.alias
9604                        .clone()
9605                        .into_iter()
9606                        .chain(core::iter::once(t.name.clone()))
9607                })
9608                .collect();
9609            let refs_list = |e: &Expr| -> bool {
9610                fn walk(e: &Expr, names: &[String]) -> bool {
9611                    match e {
9612                        Expr::Column(c) => c
9613                            .qualifier
9614                            .as_deref()
9615                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9616                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9617                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9618                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9619                        Expr::Case {
9620                            operand,
9621                            branches,
9622                            else_branch,
9623                        } => {
9624                            operand.as_deref().is_some_and(|o| walk(o, names))
9625                                || branches
9626                                    .iter()
9627                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9628                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9629                        }
9630                        _ => false,
9631                    }
9632                }
9633                walk(e, &names)
9634            };
9635            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9636                locking: None,
9637                ctes: Vec::new(),
9638                distinct: false,
9639                distinct_on: Vec::new(),
9640                items,
9641                from: Some(fc.clone()),
9642                where_: sub_where.clone(),
9643                group_by: None,
9644                group_by_all: false,
9645                having: None,
9646                unions: Vec::new(),
9647                order_by: Vec::new(),
9648                limit: None,
9649                offset: None,
9650                limit_with_ties: false,
9651                window_check_exprs: Vec::new(),
9652            };
9653            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9654            // assignment RHS with a correlated scalar subquery, instead of
9655            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9656            // column reference (`SET v = v + u.bonus`, where `v` is the target
9657            // table's column) inside a subquery whose FROM only has the source
9658            // table, so the unqualified `v` resolved against the source and
9659            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9660            // context — where they belong — fixes it; only the source columns
9661            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9662            // compound variants the leaf-walk doesn't decompose.
9663            let make_subq = |inner: Expr| {
9664                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9665                    expr: inner,
9666                    alias: None,
9667                }])))
9668            };
9669            let assignments = assignments
9670                .into_iter()
9671                .map(|(col, mut expr)| {
9672                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9673                    (col, expr)
9674                })
9675                .collect();
9676            let exists = Expr::Exists {
9677                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9678                    expr: Expr::Literal(Literal::Integer(1)),
9679                    alias: None,
9680                }])),
9681                negated: false,
9682            };
9683            // v7.39 (round 241) — RETURNING may reference the FROM-list
9684            // tables too (`RETURNING emp.id, dept.name`); the same
9685            // leaf-to-correlated-subquery lowering the assignments get.
9686            // Without it the qualifier died at eval with "unknown table
9687            // qualifier". (RETURNING was parsed before this block — the
9688            // lowering is a pure AST transformation.)
9689            if let Some(items) = returning.as_mut() {
9690                for item in items.iter_mut() {
9691                    if let SelectItem::Expr { expr, .. } = item {
9692                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9693                    }
9694                }
9695            }
9696            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9697            // EVERY matching target row: it gets no EXISTS filter, but the
9698            // caller's WHERE still applies, with source columns read through
9699            // the correlated subquery (NULL when unmatched — LEFT-join
9700            // semantics). `sub_where` above already excluded the WHERE from
9701            // the source subquery for this case.
9702            if mysql_outer {
9703                let mut outer = where_;
9704                if let Some(w) = outer.as_mut() {
9705                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9706                }
9707                (assignments, outer)
9708            } else {
9709                (assignments, Some(exists))
9710            }
9711        } else {
9712            (assignments, where_)
9713        };
9714        Ok(Statement::Update(crate::ast::UpdateStatement {
9715            ctes: Vec::new(),
9716            table,
9717            only,
9718            alias,
9719            assignments,
9720            from_sources,
9721            where_,
9722            order_limit: update_order_limit,
9723            returning,
9724        }))
9725    }
9726
9727    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9728    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9729    /// clause and its meaning are identical, so both call this rather than
9730    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9731    /// legal. PG has no such clause on either statement, so it is read only
9732    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9733    /// errors.
9734    ///
9735    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9736    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9737    /// stack in round 430.
9738    #[inline(never)]
9739    fn parse_mysql_dml_order_limit(
9740        &mut self,
9741        what: &str,
9742    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9743        if !self.mysql_dialect {
9744            return Ok(None);
9745        }
9746        let order_by = self.parse_order_by_keys()?;
9747        let limit = if matches!(self.peek(), Token::Limit) {
9748            self.advance();
9749            let tok = self.advance();
9750            let Token::Integer(n) = tok else {
9751                return Err(self.err(alloc::format!(
9752                    "expected integer after {what} LIMIT, got {tok:?}"
9753                )));
9754            };
9755            // MySQL rejects the `LIMIT offset, count` form here — only a
9756            // single row count is legal on a DML statement.
9757            if matches!(self.peek(), Token::Comma) {
9758                return Err(self.err(alloc::format!(
9759                    "{what} LIMIT takes a row count, not an offset"
9760                )));
9761            }
9762            let n = u32::try_from(n)
9763                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9764            Some(n)
9765        } else {
9766            None
9767        };
9768        if order_by.is_empty() && limit.is_none() {
9769            return Ok(None);
9770        }
9771        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9772            order_by,
9773            limit,
9774        })))
9775    }
9776
9777    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9778    /// the leading `DELETE` ident.
9779    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9780        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9781        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9782        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9783        // parse here; it reaches the existing USING path with the target
9784        // repeated in the list, which the source-list peel below handles.)
9785        // More than one name is a multi-TARGET delete, which SPG does not
9786        // model; it is refused rather than half-applied.
9787        let mysql_pre_target: Option<String> =
9788            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9789                let first = self.expect_ident_like()?;
9790                if matches!(self.peek(), Token::Comma) {
9791                    return Err(self.err(alloc::format!(
9792                        "multi-table DELETE can only delete from one table; \
9793                     `DELETE {first}, …` names several"
9794                    )));
9795                }
9796                Some(first)
9797            } else {
9798                None
9799            };
9800        if !matches!(self.peek(), Token::From) {
9801            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9802        }
9803        self.advance();
9804        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9805        // lookahead as the UPDATE spelling.
9806        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9807            if s.eq_ignore_ascii_case("only"))
9808            && matches!(
9809                self.tokens.get(self.pos + 1),
9810                Some(Token::Ident(_) | Token::QuotedIdent(_))
9811            );
9812        if only {
9813            self.advance();
9814        }
9815        let table = self.expect_ident_like()?;
9816        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9817        // spelling must not swallow the clause keywords that can follow
9818        // the target.
9819        let alias = if matches!(self.peek(), Token::As) {
9820            self.advance();
9821            Some(self.expect_ident_like()?)
9822        } else {
9823            match self.peek() {
9824                Token::Ident(s) | Token::QuotedIdent(s)
9825                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9826                {
9827                    let a = s.clone();
9828                    self.advance();
9829                    Some(a)
9830                }
9831                _ => None,
9832            }
9833        };
9834        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9835        // through the SAME join grammar the FROM clause uses (see the
9836        // `advance()`-destroys-tokens note on `parse_from_joins`).
9837        let mut mysql_on: Option<Expr> = None;
9838        let mut mysql_outer = false;
9839        let mysql_using = if mysql_pre_target.is_some()
9840            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9841        {
9842            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9843            let mut joins = self.parse_from_joins(&target_qual)?;
9844            if joins.is_empty() {
9845                return Err(self.err(alloc::string::String::from(
9846                    "multi-table DELETE needs at least one source table",
9847                )));
9848            }
9849            let head = joins.remove(0);
9850            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9851            mysql_on = head.on;
9852            Some(FromClause {
9853                primary: head.table,
9854                joins,
9855            })
9856        } else {
9857            None
9858        };
9859        // The pre-FROM target must be the table the FROM names (or its
9860        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9861        // is not the scan target.
9862        if let Some(t) = &mysql_pre_target {
9863            let names_target = t.eq_ignore_ascii_case(&table)
9864                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9865            if !names_target {
9866                return Err(self.err(alloc::format!(
9867                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9868                )));
9869            }
9870        }
9871        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9872        // delete. Same lowering as UPDATE … FROM: the WHERE
9873        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9874        // target row by the correlated machinery.
9875        let using_clause = if let Some(fc) = mysql_using {
9876            Some(fc)
9877        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9878            self.advance();
9879            let mut fc = self.parse_from_clause()?;
9880            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9881            // repeats the TARGET as the first USING entry (PG's spelling
9882            // lists only the extra sources). Peel it so the source subquery
9883            // does not re-scan — and shadow — the target table.
9884            let primary_is_target =
9885                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9886            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9887                let head = fc.joins.remove(0);
9888                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9889                mysql_on = head.on;
9890                fc = FromClause {
9891                    primary: head.table,
9892                    joins: fc.joins,
9893                };
9894            }
9895            Some(fc)
9896        } else {
9897            None
9898        };
9899        let where_ = if matches!(self.peek(), Token::Where) {
9900            self.advance();
9901            Some(self.parse_expr(0)?)
9902        } else {
9903            None
9904        };
9905        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9906        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9907        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9908        let mut returning = self.parse_optional_returning()?;
9909        let where_ = if let Some(fc) = using_clause {
9910            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9911            // a USING-table reference in RETURNING becomes a correlated
9912            // scalar subquery over the USING list.
9913            let names: Vec<String> = core::iter::once(&fc.primary)
9914                .chain(fc.joins.iter().map(|j| &j.table))
9915                .flat_map(|t| {
9916                    t.alias
9917                        .clone()
9918                        .into_iter()
9919                        .chain(core::iter::once(t.name.clone()))
9920                })
9921                .collect();
9922            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9923            // join filters the SOURCE subquery on the ON predicate alone and
9924            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9925            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9926            // rows); every other form folds ON and WHERE into one EXISTS.
9927            let sub_where = match (mysql_on.clone(), where_.clone()) {
9928                _ if mysql_outer => mysql_on.clone(),
9929                (Some(on), Some(w)) => Some(Expr::Binary {
9930                    lhs: Box::new(on),
9931                    op: crate::ast::BinOp::And,
9932                    rhs: Box::new(w),
9933                }),
9934                (Some(on), None) => Some(on),
9935                (None, w) => w,
9936            };
9937            let exists_where = sub_where.clone();
9938            let sub_fc = fc.clone();
9939            let make_subq = move |leaf: Expr| -> Expr {
9940                Expr::ScalarSubquery(Box::new(SelectStatement {
9941                    locking: None,
9942                    ctes: Vec::new(),
9943                    distinct: false,
9944                    distinct_on: Vec::new(),
9945                    items: alloc::vec![SelectItem::Expr {
9946                        expr: leaf,
9947                        alias: None,
9948                    }],
9949                    from: Some(sub_fc.clone()),
9950                    where_: sub_where.clone(),
9951                    group_by: None,
9952                    group_by_all: false,
9953                    having: None,
9954                    unions: Vec::new(),
9955                    order_by: Vec::new(),
9956                    limit: None,
9957                    offset: None,
9958                    limit_with_ties: false,
9959                    window_check_exprs: Vec::new(),
9960                }))
9961            };
9962            let refs = |e: &Expr| expr_refs_tables(e, &names);
9963            if let Some(items) = returning.as_mut() {
9964                for item in items.iter_mut() {
9965                    if let SelectItem::Expr { expr, .. } = item {
9966                        wrap_from_leaves(expr, &names, &make_subq, &refs);
9967                    }
9968                }
9969            }
9970            // A LEFT join deletes the target rows the WHERE selects, reading
9971            // source columns through the correlated subquery (NULL when
9972            // unmatched); no EXISTS row filter.
9973            if mysql_outer {
9974                let mut outer = where_;
9975                if let Some(w) = outer.as_mut() {
9976                    wrap_from_leaves(w, &names, &make_subq, &refs);
9977                }
9978                outer
9979            } else {
9980                Some(Expr::Exists {
9981                    subquery: Box::new(SelectStatement {
9982                        locking: None,
9983                        ctes: Vec::new(),
9984                        distinct: false,
9985                        distinct_on: Vec::new(),
9986                        items: alloc::vec![SelectItem::Expr {
9987                            expr: Expr::Literal(Literal::Integer(1)),
9988                            alias: None,
9989                        }],
9990                        from: Some(fc),
9991                        where_: exists_where,
9992                        group_by: None,
9993                        group_by_all: false,
9994                        having: None,
9995                        unions: Vec::new(),
9996                        order_by: Vec::new(),
9997                        limit: None,
9998                        offset: None,
9999                        limit_with_ties: false,
10000                        window_check_exprs: Vec::new(),
10001                    }),
10002                    negated: false,
10003                })
10004            }
10005        } else {
10006            where_
10007        };
10008        Ok(Statement::Delete(crate::ast::DeleteStatement {
10009            ctes: Vec::new(),
10010            table,
10011            only,
10012            alias,
10013            where_,
10014            order_limit: delete_order_limit,
10015            returning,
10016        }))
10017    }
10018
10019    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
10020    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
10021    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
10022    /// keyword. v7.17 surface:
10023    ///   * source: table reference (subquery source is a follow-up)
10024    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
10025    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
10026    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
10027    ///     order
10028    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
10029        // INTO
10030        let is_into_kw = matches!(self.peek(), Token::Into)
10031            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
10032        if !is_into_kw {
10033            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
10034        }
10035        self.advance();
10036        let target = self.expect_ident_like()?;
10037        // Optional alias — bare ident before USING.
10038        let target_alias = match self.peek() {
10039            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10040                Some(self.expect_ident_like()?)
10041            }
10042            _ => None,
10043        };
10044        // USING
10045        let is_using_kw = matches!(
10046            self.peek(),
10047            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10048        );
10049        if !is_using_kw {
10050            return Err(self.err(format!(
10051                "expected USING after MERGE INTO target, got {:?}",
10052                self.peek()
10053            )));
10054        }
10055        self.advance();
10056        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10057        // <table> [alias]`. PG requires an alias after a subquery source.
10058        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10059            self.advance(); // (
10060            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10061            // constant-SELECT lowering the derived-table parser uses
10062            // (PG deletes through this form; it was a parse error).
10063            let inner = if matches!(self.peek(), Token::Values) {
10064                self.advance(); // VALUES
10065                Statement::Select(self.parse_values_rows_body()?)
10066            } else {
10067                self.parse_select_stmt()?
10068            };
10069            match self.advance() {
10070                Token::RParen => {}
10071                other => {
10072                    return Err(self.err(format!(
10073                        "expected ')' after MERGE USING subquery, got {other:?}"
10074                    )));
10075                }
10076            }
10077            let Statement::Select(sub) = inner else {
10078                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10079            };
10080            (String::new(), Some(Box::new(sub)))
10081        } else {
10082            (self.expect_ident_like()?, None)
10083        };
10084        let source_alias = match self.peek() {
10085            Token::Ident(s) | Token::QuotedIdent(s)
10086                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10087            {
10088                Some(self.expect_ident_like()?)
10089            }
10090            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10091                self.advance(); // AS
10092                Some(self.expect_ident_like()?)
10093            }
10094            _ => None,
10095        };
10096        // v7.39 (round 768, F31-D5) — optional positional column-alias
10097        // list after the source alias (`s(id, v)`).
10098        let mut source_column_aliases: Vec<String> = Vec::new();
10099        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10100            self.advance();
10101            loop {
10102                source_column_aliases.push(self.expect_ident_like()?);
10103                match self.peek() {
10104                    Token::Comma => {
10105                        self.advance();
10106                    }
10107                    Token::RParen => {
10108                        self.advance();
10109                        break;
10110                    }
10111                    other => {
10112                        return Err(self.err(format!(
10113                            "expected ',' or ')' in MERGE source column list, got {other:?}"
10114                        )));
10115                    }
10116                }
10117            }
10118        }
10119        if source_select.is_some() && source_alias.is_none() {
10120            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10121        }
10122        // ON
10123        if !matches!(self.peek(), Token::On) {
10124            return Err(self.err(format!(
10125                "expected ON after MERGE … USING source, got {:?}",
10126                self.peek()
10127            )));
10128        }
10129        self.advance();
10130        let on = self.parse_expr(0)?;
10131        // One or more WHEN clauses.
10132        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10133        loop {
10134            let is_when_kw = matches!(
10135                self.peek(),
10136                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10137            );
10138            if !is_when_kw {
10139                break;
10140            }
10141            self.advance(); // WHEN
10142            // [NOT] MATCHED
10143            let matched = if matches!(self.peek(), Token::Not) {
10144                self.advance();
10145                crate::ast::MergeMatched::NotMatched
10146            } else {
10147                crate::ast::MergeMatched::Matched
10148            };
10149            let is_matched_kw = matches!(
10150                self.peek(),
10151                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10152            );
10153            if !is_matched_kw {
10154                return Err(self.err(format!(
10155                    "expected MATCHED in WHEN clause, got {:?}",
10156                    self.peek()
10157                )));
10158            }
10159            self.advance();
10160            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10161            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10162            // to fire for target rows no source row matches.
10163            let mut matched = matched;
10164            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10165                self.advance();
10166                match self.peek() {
10167                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10168                        self.advance();
10169                        matched = crate::ast::MergeMatched::NotMatchedBySource;
10170                    }
10171                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10172                        self.advance();
10173                    }
10174                    other => {
10175                        return Err(self.err(format!(
10176                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10177                        )));
10178                    }
10179                }
10180            }
10181            // Optional AND <expr>
10182            let condition = if matches!(self.peek(), Token::And) {
10183                self.advance();
10184                Some(self.parse_expr(0)?)
10185            } else {
10186                None
10187            };
10188            // THEN
10189            let is_then_kw = matches!(
10190                self.peek(),
10191                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10192            );
10193            if !is_then_kw {
10194                return Err(self.err(format!(
10195                    "expected THEN in WHEN clause, got {:?}",
10196                    self.peek()
10197                )));
10198            }
10199            self.advance();
10200            // Action: INSERT / UPDATE / DELETE / DO NOTHING
10201            let action = match self.peek().clone() {
10202                Token::Insert => {
10203                    self.advance();
10204                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10205                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10206                    // VALUES (…)` omits it and fills every column in declaration
10207                    // order. PG accepts this; SPG used to require the list.
10208                    let mut columns: Vec<String> = Vec::new();
10209                    if matches!(self.peek(), Token::LParen) {
10210                        self.advance();
10211                        loop {
10212                            columns.push(self.expect_ident_like()?);
10213                            if matches!(self.peek(), Token::Comma) {
10214                                self.advance();
10215                                continue;
10216                            }
10217                            break;
10218                        }
10219                        if !matches!(self.peek(), Token::RParen) {
10220                            return Err(self.err(format!(
10221                                "expected ')' after INSERT column list, got {:?}",
10222                                self.peek()
10223                            )));
10224                        }
10225                        self.advance();
10226                    }
10227                    // VALUES (...)
10228                    if !matches!(self.peek(), Token::Values) {
10229                        return Err(self.err(format!(
10230                            "expected VALUES in MERGE INSERT, got {:?}",
10231                            self.peek()
10232                        )));
10233                    }
10234                    self.advance();
10235                    if !matches!(self.peek(), Token::LParen) {
10236                        return Err(self.err(format!(
10237                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
10238                            self.peek()
10239                        )));
10240                    }
10241                    self.advance();
10242                    let mut values: Vec<crate::ast::Expr> = Vec::new();
10243                    loop {
10244                        values.push(self.parse_expr(0)?);
10245                        if matches!(self.peek(), Token::Comma) {
10246                            self.advance();
10247                            continue;
10248                        }
10249                        break;
10250                    }
10251                    if !matches!(self.peek(), Token::RParen) {
10252                        return Err(self.err(format!(
10253                            "expected ')' after MERGE INSERT values, got {:?}",
10254                            self.peek()
10255                        )));
10256                    }
10257                    self.advance();
10258                    // Empty column list = positional into every column, so the
10259                    // count is checked against the table arity at execution.
10260                    if !columns.is_empty() && columns.len() != values.len() {
10261                        return Err(self.err(format!(
10262                            "MERGE INSERT column count ({}) ≠ value count ({})",
10263                            columns.len(),
10264                            values.len()
10265                        )));
10266                    }
10267                    crate::ast::MergeAction::Insert { columns, values }
10268                }
10269                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10270                    self.advance();
10271                    // SET
10272                    let is_set_kw = matches!(
10273                        self.peek(),
10274                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10275                    );
10276                    if !is_set_kw {
10277                        return Err(self.err(format!(
10278                            "expected SET after UPDATE in MERGE, got {:?}",
10279                            self.peek()
10280                        )));
10281                    }
10282                    self.advance();
10283                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10284                    loop {
10285                        let col = self.expect_ident_like()?;
10286                        if !matches!(self.peek(), Token::Eq) {
10287                            return Err(self.err(format!(
10288                                "expected '=' in MERGE UPDATE assignment, got {:?}",
10289                                self.peek()
10290                            )));
10291                        }
10292                        self.advance();
10293                        let expr = self.parse_expr(0)?;
10294                        assignments.push((col, expr));
10295                        if matches!(self.peek(), Token::Comma) {
10296                            self.advance();
10297                            continue;
10298                        }
10299                        break;
10300                    }
10301                    crate::ast::MergeAction::Update { assignments }
10302                }
10303                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10304                    self.advance();
10305                    crate::ast::MergeAction::Delete
10306                }
10307                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10308                    self.advance();
10309                    let is_nothing_kw = matches!(
10310                        self.peek(),
10311                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10312                    );
10313                    if !is_nothing_kw {
10314                        return Err(self.err(format!(
10315                            "expected NOTHING after DO in MERGE clause, got {:?}",
10316                            self.peek()
10317                        )));
10318                    }
10319                    self.advance();
10320                    crate::ast::MergeAction::DoNothing
10321                }
10322                other => {
10323                    return Err(self.err(format!(
10324                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10325                    )));
10326                }
10327            };
10328            // PG's grammar simply has no INSERT production under BY SOURCE
10329            // (a target row already exists there) — same syntax error.
10330            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10331                && matches!(action, crate::ast::MergeAction::Insert { .. })
10332            {
10333                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10334            }
10335            clauses.push(crate::ast::MergeWhenClause {
10336                matched,
10337                condition,
10338                action,
10339            });
10340        }
10341        if clauses.is_empty() {
10342            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10343        }
10344        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10345        // unconditional (no `AND`) WHEN of the same match kind: it could
10346        // never fire. Check per match kind in clause order.
10347        let mut seen_unconditional_matched = false;
10348        let mut seen_unconditional_not_matched = false;
10349        let mut seen_unconditional_by_source = false;
10350        for c in &clauses {
10351            let seen = match c.matched {
10352                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10353                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10354                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10355            };
10356            if *seen {
10357                return Err(self.err(String::from(
10358                    "unreachable WHEN clause specified after unconditional WHEN clause",
10359                )));
10360            }
10361            if c.condition.is_none() {
10362                *seen = true;
10363            }
10364        }
10365        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10366        let returning = self.parse_optional_returning()?;
10367        Ok(Statement::Merge(crate::ast::MergeStatement {
10368            // Attached by `parse_with_cte_then_select` when the MERGE
10369            // heads a WITH clause (round 149).
10370            ctes: Vec::new(),
10371            target,
10372            target_alias,
10373            source,
10374            source_alias,
10375            source_select,
10376            source_column_aliases,
10377            on,
10378            clauses,
10379            returning,
10380        }))
10381    }
10382
10383    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10384    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10385    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10386    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10387    fn parse_optional_returning(
10388        &mut self,
10389    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10390        let is_returning_kw = matches!(
10391            self.peek(),
10392            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10393        );
10394        if !is_returning_kw {
10395            return Ok(None);
10396        }
10397        self.advance();
10398        let mut items = Vec::new();
10399        loop {
10400            items.push(self.parse_select_item()?);
10401            if matches!(self.peek(), Token::Comma) {
10402                self.advance();
10403                continue;
10404            }
10405            break;
10406        }
10407        Ok(Some(items))
10408    }
10409
10410    /// v6.0.4 — parse the tail of an ALTER statement after the
10411    /// leading `ALTER` keyword has been consumed. Only one form is
10412    /// supported in v6.0.4:
10413    ///
10414    /// ```text
10415    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10416    /// ```
10417    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10418        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10419        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10420        // exclusion) is accepted by stripping the `ONLY` keyword
10421        // before the table parse.
10422        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10423        // and the long PG-dump tail are accepted as no-ops.
10424        match self.advance() {
10425            Token::Index => {}
10426            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10427            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10428            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10429            Token::Table => {
10430                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10431                    self.advance();
10432                }
10433                return self.parse_alter_table_after_keyword();
10434            }
10435            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10436                return self.parse_alter_policy_after_keyword();
10437            }
10438            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10439                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10440                    self.advance();
10441                }
10442                return self.parse_alter_table_after_keyword();
10443            }
10444            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10445            // of the silent-noop tail.
10446            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10447                return self.parse_alter_sequence_after_keyword();
10448            }
10449            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10450            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10451            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10452            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10453                // NB: the match arm consumed `TYPE` via self.advance(); the
10454                // cursor is now at the type name — do NOT advance again.
10455                let type_name = self.expect_ident_like()?;
10456                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10457                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10458                if is_add_value {
10459                    self.advance(); // ADD
10460                    self.advance(); // VALUE
10461                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10462                    // IF/EXISTS as identifiers.
10463                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10464                    {
10465                        let n1 = self.tokens.get(self.pos + 1);
10466                        let n2 = self.tokens.get(self.pos + 2);
10467                        if matches!(n1, Some(Token::Not))
10468                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10469                        {
10470                            self.advance();
10471                            self.advance();
10472                            self.advance();
10473                            true
10474                        } else {
10475                            false
10476                        }
10477                    } else {
10478                        false
10479                    };
10480                    let label = self.expect_string_literal()?;
10481                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10482                    {
10483                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10484                        self.advance();
10485                        let anchor = self.expect_string_literal()?;
10486                        Some((is_before, anchor))
10487                    } else {
10488                        None
10489                    };
10490                    return Ok(Statement::AlterTypeAddValue {
10491                        type_name,
10492                        label,
10493                        if_not_exists,
10494                        position,
10495                    });
10496                }
10497                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10498                // Used to fall into the no-op tail below: accepted, silently
10499                // ignored. `RENAME TO <newtype>` keeps falling through.
10500                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10501                    && matches!(
10502                        self.tokens.get(self.pos + 1),
10503                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10504                    )
10505                {
10506                    self.advance(); // RENAME
10507                    self.advance(); // VALUE
10508                    let old = self.expect_string_literal()?;
10509                    if matches!(self.peek(), Token::To) {
10510                        self.advance();
10511                    } else {
10512                        self.expect_keyword_ident("to")?;
10513                    }
10514                    let new = self.expect_string_literal()?;
10515                    return Ok(Statement::AlterTypeRenameValue {
10516                        type_name,
10517                        old,
10518                        new,
10519                    });
10520                }
10521                // Other ALTER TYPE forms — the ACTION stays a no-op
10522                // (pg_dump tail), but v7.39 (round 708) the NAME is
10523                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10524                // success for a type that does not exist.
10525                self.consume_until_statement_boundary();
10526                return Ok(Statement::ValidateOnly {
10527                    kind: crate::ast::ValidateOnlyKind::TypeName,
10528                    names: alloc::vec![type_name],
10529                });
10530            }
10531            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10532            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10533            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10534            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10535            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10536            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10537            // pg_dump no-op list below: every form used to report success
10538            // and change nothing, which is worse than refusing outright
10539            // (a migration dropping a constraint kept rejecting data).
10540            // NOTE: the enclosing `match self.advance()` already consumed
10541            // the DOMAIN keyword, so the name is next.
10542            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10543                return self.parse_alter_domain_after_keyword();
10544            }
10545            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10546            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10547            // used to fall into the pg_dump no-op tail below, so a DBA
10548            // setting a per-role default was told it worked and nothing
10549            // happened. Intercepted here, BEFORE that tail.
10550            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10551            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10552            // interception below exists: swallowed with the no-op tail, an
10553            // unknown parameter name was ACCEPTED where PG18 answers
10554            // `unrecognized configuration parameter`. SPG applies nothing
10555            // either way — there is no postgresql.auto.conf — but it now
10556            // says so about a name it does not know.
10557            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10558                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10559                // already consumed here. An extra advance eats the SET and
10560                // the parameter name is never seen — which is exactly the
10561                // bug a panic in this branch disproved: the branch WAS on
10562                // the path, the reading of it was wrong.
10563                let mut parameter = None;
10564                // SET <name> … | RESET <name> | RESET ALL
10565                if matches!(self.peek(), Token::Ident(k)
10566                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10567                {
10568                    self.advance();
10569                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10570                        && !n.eq_ignore_ascii_case("all")
10571                    {
10572                        self.advance();
10573                        // A dotted GUC (`plpgsql.check_asserts`) is two
10574                        // tokens; keep the whole name.
10575                        let mut full = n;
10576                        while matches!(self.peek(), Token::Dot) {
10577                            self.advance();
10578                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10579                                full.push('.');
10580                                full.push_str(&t);
10581                            }
10582                        }
10583                        parameter = Some(full);
10584                    }
10585                }
10586                self.consume_until_statement_boundary();
10587                return Ok(Statement::AlterSystem { parameter });
10588            }
10589            Token::Ident(s) | Token::QuotedIdent(s)
10590                if matches!(
10591                    s.to_ascii_lowercase().as_str(),
10592                    "role" | "user" | "database"
10593                ) && self.peeks_db_role_setting() =>
10594            {
10595                let is_database = s.eq_ignore_ascii_case("database");
10596                return self.parse_db_role_setting(is_database);
10597            }
10598            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10599            // (the non-SET forms; SET/RESET took the branch above). The
10600            // attributes still no-op — recorded, and the ignored PASSWORD
10601            // is ledgered as its own follow-up — but the ROLE is validated:
10602            // any name was accepted for a role that does not exist.
10603            Token::Ident(s) | Token::QuotedIdent(s)
10604                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10605            {
10606                // NB: the enclosing `match self.advance()` already consumed
10607                // ROLE/USER — the round-695 trap, hit again in this round's
10608                // first draft (the name was eaten and WITH parsed as the
10609                // role). The cursor is at the name.
10610                let name = self.expect_ident_or_string()?;
10611                // v7.39 (round 750) — scan the attribute tail for
10612                // PASSWORD. Everything else stays a recorded no-op, but
10613                // a dropped credential rotation is a SECURITY bug:
10614                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10615                // changed nothing, so the old password kept working.
10616                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10617                // NULL` clears the credential.
10618                let mut password: Option<Option<String>> = None;
10619                loop {
10620                    match self.peek() {
10621                        Token::Semicolon | Token::Eof => break,
10622                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10623                            self.advance();
10624                            match self.advance() {
10625                                Token::String(p) => password = Some(Some(p)),
10626                                Token::Null => password = Some(None),
10627                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10628                                    password = Some(None);
10629                                }
10630                                other => {
10631                                    return Err(self.err(alloc::format!(
10632                                        "expected password string or NULL after PASSWORD, got {other:?}"
10633                                    )));
10634                                }
10635                            }
10636                        }
10637                        _ => {
10638                            self.advance();
10639                        }
10640                    }
10641                }
10642                if name.eq_ignore_ascii_case("all") {
10643                    // `ALTER ROLE ALL …` names every role; nothing to check.
10644                    return Ok(Statement::Empty);
10645                }
10646                if let Some(pw) = password {
10647                    return Ok(Statement::AlterRolePassword { name, password: pw });
10648                }
10649                return Ok(Statement::ValidateOnly {
10650                    kind: crate::ast::ValidateOnlyKind::RoleName,
10651                    names: alloc::vec![name],
10652                });
10653            }
10654            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10655            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10656            // list far enough to validate the NAME; the actions still no-op.
10657            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10658            // models none of them and their dumps are rare.)
10659            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10660                let name = self.expect_ident_or_string()?;
10661                self.consume_until_statement_boundary();
10662                return Ok(Statement::ValidateOnly {
10663                    kind: crate::ast::ValidateOnlyKind::CollationName,
10664                    names: alloc::vec![name],
10665                });
10666            }
10667            Token::Ident(s) | Token::QuotedIdent(s)
10668                if s.eq_ignore_ascii_case("text")
10669                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10670                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10671            {
10672                self.advance(); // SEARCH
10673                self.advance(); // CONFIGURATION
10674                let name = self.expect_ident_like()?;
10675                self.consume_until_statement_boundary();
10676                return Ok(Statement::ValidateOnly {
10677                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10678                    names: alloc::vec![name],
10679                });
10680            }
10681            Token::Ident(s) | Token::QuotedIdent(s)
10682                if s.eq_ignore_ascii_case("event")
10683                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10684            {
10685                self.advance(); // TRIGGER
10686                let name = self.expect_ident_like()?;
10687                self.consume_until_statement_boundary();
10688                return Ok(Statement::ValidateOnly {
10689                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10690                    names: alloc::vec![name],
10691                });
10692            }
10693            Token::Ident(s) | Token::QuotedIdent(s)
10694                if s.eq_ignore_ascii_case("large")
10695                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10696            {
10697                self.advance(); // OBJECT
10698                let oid = match self.advance() {
10699                    Token::Integer(n) => alloc::format!("{n}"),
10700                    other => {
10701                        return Err(
10702                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10703                        );
10704                    }
10705                };
10706                self.consume_until_statement_boundary();
10707                return Ok(Statement::ValidateOnly {
10708                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10709                    names: alloc::vec![oid],
10710                });
10711            }
10712            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10713            // argument-list parse as DROP AGGREGATE (round 707); the
10714            // action no-ops, the existence check is real.
10715            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10716                // Same round-695 trap as above: AGGREGATE is already
10717                // consumed; the cursor is at the name.
10718                let name = self.expect_ident_like()?;
10719                let mut names = alloc::vec![name];
10720                if matches!(self.peek(), Token::LParen) {
10721                    self.advance();
10722                    loop {
10723                        match self.peek().clone() {
10724                            Token::RParen => {
10725                                self.advance();
10726                                break;
10727                            }
10728                            Token::Star => {
10729                                self.advance();
10730                                names.push(String::from("*"));
10731                            }
10732                            Token::Comma => {
10733                                self.advance();
10734                            }
10735                            _ => {
10736                                let mut t = self.expect_ident_like()?;
10737                                while let Token::Ident(nx) = self.peek() {
10738                                    let nx = nx.clone();
10739                                    self.advance();
10740                                    t.push(' ');
10741                                    t.push_str(&nx);
10742                                }
10743                                names.push(t);
10744                            }
10745                        }
10746                    }
10747                }
10748                self.consume_until_statement_boundary();
10749                return Ok(Statement::ValidateOnly {
10750                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10751                    names,
10752                });
10753            }
10754            Token::Ident(s) | Token::QuotedIdent(s)
10755                if matches!(
10756                    s.to_ascii_lowercase().as_str(),
10757                    "view"
10758                        | "function"
10759                        | "database"
10760                        | "schema"
10761                        | "owner"
10762                        | "default"
10763                        | "extension"
10764                        | "materialized"
10765                        | "publication"
10766                        | "subscription"
10767                        // v7.37.17 (17.6 siblings) — additional ALTER
10768                        // targets pg_dump / pg_dumpall / operator DB
10769                        // migration scripts commonly emit. SPG has
10770                        // no matching machinery for any of these; the
10771                        // parser accepts + Empty-returns so pg_dump
10772                        // tail statements don't stall.
10773                        | "tablespace"
10774                        | "language"
10775                        | "operator"
10776                        | "conversion"
10777                        | "statistics"
10778                        | "server"
10779                        | "foreign"
10780                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10781                        // / TEMPLATE (CONFIGURATION intercepted above).
10782                        | "text"
10783                ) =>
10784            {
10785                self.consume_until_statement_boundary();
10786                return Ok(Statement::Empty);
10787            }
10788            other => {
10789                return Err(self.err(format!(
10790                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10791                     after ALTER, got {other:?}"
10792                )));
10793            }
10794        }
10795        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10796        // (mailrs migrate-042 ships these). The presence of an
10797        // IF EXISTS makes the subsequent name lookup tolerate
10798        // a missing index — engine returns CommandOk no-op.
10799        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10800            let next = self.tokens.get(self.pos + 1);
10801            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10802                self.advance();
10803                self.advance();
10804                true
10805            } else {
10806                false
10807            }
10808        } else {
10809            false
10810        };
10811        let name = self.expect_ident_like()?;
10812        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10813        // Detect BEFORE the REBUILD path so the existing REBUILD
10814        // arm stays untouched.
10815        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10816            self.advance();
10817            if matches!(self.peek(), Token::To) {
10818                self.advance();
10819            } else {
10820                self.expect_keyword_ident("to")?;
10821            }
10822            let new = self.expect_ident_like()?;
10823            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10824                name,
10825                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10826            }));
10827        }
10828        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10829        // A syntax error before; the index is validated, the params no-op.
10830        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10831            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10832                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10833        {
10834            self.consume_until_statement_boundary();
10835            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10836                name,
10837                target: crate::ast::AlterIndexTarget::StorageParams,
10838            }));
10839        }
10840        // REBUILD
10841        self.expect_keyword_ident("rebuild")?;
10842        // Optional: WITH (encoding = <enc>)
10843        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10844            self.advance();
10845            if !matches!(self.peek(), Token::LParen) {
10846                return Err(self.err(format!(
10847                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10848                    self.peek()
10849                )));
10850            }
10851            self.advance();
10852            self.expect_keyword_ident("encoding")?;
10853            if !matches!(self.peek(), Token::Eq) {
10854                return Err(self.err(format!(
10855                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10856                    self.peek()
10857                )));
10858            }
10859            self.advance();
10860            let enc_ident = match self.advance() {
10861                Token::Ident(s) | Token::QuotedIdent(s) => s,
10862                other => {
10863                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10864                }
10865            };
10866            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10867                "f32" => VecEncoding::F32,
10868                "sq8" => VecEncoding::Sq8,
10869                "half" => VecEncoding::F16,
10870                other => {
10871                    return Err(self.err(format!(
10872                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10873                    )));
10874                }
10875            };
10876            if !matches!(self.peek(), Token::RParen) {
10877                return Err(self.err(format!(
10878                    "expected ')' after encoding value, got {:?}",
10879                    self.peek()
10880                )));
10881            }
10882            self.advance();
10883            Some(enc)
10884        } else {
10885            None
10886        };
10887        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10888            name,
10889            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10890        }))
10891    }
10892
10893    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10894    /// only `SET` form currently supported; future v6.7.x can add
10895    /// more SET subjects without changing the dispatch shape.
10896    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10897    /// subactions. Single-subaction shape stays a 1-element vec.
10898    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10899        let table_name = self.expect_ident_like()?;
10900        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10901        loop {
10902            let subaction = self.parse_alter_table_subaction()?;
10903            // ADD COLUMN with inline REFERENCES emits both an
10904            // AddColumn and an AddForeignKey subaction; the
10905            // helper returns 1 or 2 items.
10906            targets.extend(subaction);
10907            if matches!(self.peek(), Token::Comma) {
10908                self.advance();
10909                continue;
10910            }
10911            break;
10912        }
10913        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10914            name: table_name,
10915            targets,
10916        }))
10917    }
10918
10919    /// Parse one ALTER TABLE subaction. Returns a Vec because
10920    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10921    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10922    fn parse_alter_table_subaction(
10923        &mut self,
10924    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10925        match self.peek() {
10926            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
10927                self.advance();
10928                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
10929                // storage parameters: paren-prefixed; consume.
10930                if matches!(self.peek(), Token::LParen) {
10931                    self.consume_until_statement_boundary();
10932                    return Ok(Vec::new());
10933                }
10934                let setting = self.expect_ident_like()?;
10935                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
10936                    if !matches!(self.peek(), Token::Eq) {
10937                        return Err(self.err(alloc::format!(
10938                            "expected '=' after hot_tier_bytes, got {:?}",
10939                            self.peek()
10940                        )));
10941                    }
10942                    self.advance();
10943                    let n = self.expect_u64_literal()?;
10944                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
10945                }
10946                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
10947                // accept-and-no-op for ALTER TABLE SET <subject>
10948                // forms that pg_dump emits but SPG either treats
10949                // as N/A (single-tenant, single-owner, no shared
10950                // tablespaces) or accepts the dump-side declaration
10951                // without runtime effect:
10952                //   SET SCHEMA <name>            (18.11)
10953                //   SET TABLESPACE <name>        (18.8)
10954                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
10955                //   SET WITHOUT CLUSTER          (18.13)
10956                //   SET WITHOUT OIDS             (PG legacy)
10957                //   SET (option = value, …)      (storage parameters)
10958                //   SET REPLICA IDENTITY {…}     (18.14)
10959                if setting.eq_ignore_ascii_case("schema")
10960                    || setting.eq_ignore_ascii_case("tablespace")
10961                    || setting.eq_ignore_ascii_case("logged")
10962                    || setting.eq_ignore_ascii_case("unlogged")
10963                    || setting.eq_ignore_ascii_case("without")
10964                {
10965                    self.consume_until_statement_boundary();
10966                    return Ok(Vec::new());
10967                }
10968                if setting.eq_ignore_ascii_case("replica") {
10969                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
10970                    self.consume_until_statement_boundary();
10971                    return Ok(Vec::new());
10972                }
10973                // SET (option=value, …) — storage parameters.
10974                if matches!(self.peek(), Token::LParen) {
10975                    self.consume_until_statement_boundary();
10976                    return Ok(Vec::new());
10977                }
10978                Err(self.err(alloc::format!(
10979                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
10980                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
10981                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
10982                )))
10983            }
10984            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
10985            // not ignored: round 645 gave SPG the inheritance the
10986            // v7.37.18 no-op said it did not have.
10987            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
10988                self.advance();
10989                let parent = self.expect_ident_like()?;
10990                self.consume_until_statement_boundary();
10991                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
10992                    parent,
10993                    detach: false
10994                }])
10995            }
10996            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
10997            // LEVEL SECURITY`, which has its own RLS arm below — without
10998            // the guard this swallowed NO FORCE as a no-op.
10999            Token::Ident(s)
11000                if s.eq_ignore_ascii_case("no")
11001                    && !matches!(
11002                        self.tokens.get(self.pos + 1),
11003                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11004                    ) =>
11005            {
11006                self.advance();
11007                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
11008                    if k.eq_ignore_ascii_case("inherit"))
11009                {
11010                    self.advance();
11011                    let parent = self.expect_ident_like()?;
11012                    self.consume_until_statement_boundary();
11013                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11014                        parent,
11015                        detach: true
11016                    }]);
11017                }
11018                self.consume_until_statement_boundary();
11019                Ok(Vec::new())
11020            }
11021            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
11022            // single-owner, so there is still nothing to record.
11023            //
11024            // v7.39 (round 652) — but the name now reaches the engine,
11025            // which refuses a role that does not exist as PG does. The
11026            // no-op was swallowing the whole statement, so a dump naming
11027            // a role this server never heard of restored clean and left
11028            // the table owned by whoever ran the restore.
11029            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
11030                self.advance();
11031                if matches!(self.peek(), Token::To) {
11032                    self.advance();
11033                }
11034                let role = self.expect_ident_like()?;
11035                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
11036                    role
11037                }])
11038            }
11039            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11040            // PG sets a hint; SPG doesn't have clustered storage, so the
11041            // hint itself stays a no-op.
11042            //
11043            // v7.39 (round 652) — the index name is checked now. PG
11044            // errors on one that does not exist, and swallowing that let
11045            // a typo'd CLUSTER ON pass silently.
11046            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11047                self.advance();
11048                // `ON` is a reserved token, not an ident.
11049                if !matches!(self.peek(), Token::On) {
11050                    return Err(self.err(alloc::format!(
11051                        "expected ON after CLUSTER, got {:?}",
11052                        self.peek()
11053                    )));
11054                }
11055                self.advance();
11056                let index = self.expect_ident_like()?;
11057                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11058                    index: Some(index)
11059                }])
11060            }
11061            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11062            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11063            // what a logical decoder puts in the old-tuple image; SPG's
11064            // replication is SQL-text, so there is nothing to record.
11065            // Accept-and-no-op (it used to be a parse error).
11066            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11067                self.advance();
11068                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11069                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11070                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11071                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11072                {
11073                    self.advance(); // IDENTITY
11074                    self.advance(); // USING
11075                    if matches!(self.peek(), Token::Index)
11076                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11077                    {
11078                        self.advance();
11079                    }
11080                    let index = self.expect_ident_like()?;
11081                    self.consume_until_statement_boundary();
11082                    return Ok(alloc::vec![
11083                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11084                    ]);
11085                }
11086                self.consume_until_statement_boundary();
11087                Ok(Vec::new())
11088            }
11089            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11090            //
11091            // v7.39 (round 652) — it used to consume the statement and
11092            // return nothing, on the stated theory that SPG validated at
11093            // ADD CONSTRAINT time so there was never anything left to
11094            // validate. Measured against PG18, ADD CONSTRAINT did not
11095            // scan the existing rows at all — the comment described a
11096            // property SPG did not have, which is why nobody looked. Both
11097            // halves are real now: ADD scans unless told NOT VALID, and
11098            // this scans what NOT VALID skipped.
11099            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11100                self.advance();
11101                self.expect_keyword_ident("constraint")?;
11102                let name = self.expect_ident_like()?;
11103                Ok(alloc::vec![
11104                    crate::ast::AlterTableTarget::ValidateConstraint { name }
11105                ])
11106            }
11107            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11108            // SET (option = value, …). PG uses it to clear per-table
11109            // storage params like fillfactor or autovacuum_*. SPG
11110            // engine-manages those parameters; accept-and-no-op.
11111            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11112                self.advance();
11113                self.consume_until_statement_boundary();
11114                Ok(Vec::new())
11115            }
11116            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11117            // type-of binding (PG 9.0+). SPG composite types
11118            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11119            // TABLE OF is rare and inverse of CREATE TABLE OF.
11120            // Accept-and-no-op until a customer dump round-trips it.
11121            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11122                self.advance();
11123                // v7.39 (round 710) — the type name is validated now.
11124                let type_name = self.expect_ident_like()?;
11125                self.consume_until_statement_boundary();
11126                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11127                    type_name
11128                }])
11129            }
11130            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11131            // (reserved keyword) rather than Token::Ident("not"),
11132            // so it needs its own arm. Accept-and-no-op same as OF.
11133            Token::Not => {
11134                self.advance();
11135                self.consume_until_statement_boundary();
11136                Ok(Vec::new())
11137            }
11138            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11139            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11140                self.advance();
11141                self.expect_row_level_security()?;
11142                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11143                    enabled: None,
11144                    force: Some(true),
11145                }])
11146            }
11147            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11148            Token::Ident(s)
11149                if s.eq_ignore_ascii_case("no")
11150                    && matches!(
11151                        self.tokens.get(self.pos + 1),
11152                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11153                    ) =>
11154            {
11155                self.advance(); // NO
11156                self.advance(); // FORCE
11157                self.expect_row_level_security()?;
11158                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11159                    enabled: None,
11160                    force: Some(false),
11161                }])
11162            }
11163            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11164            // (sets relrowsecurity). The guard requires the next token to be
11165            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11166            Token::Ident(s)
11167                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11168                    && matches!(
11169                        self.tokens.get(self.pos + 1),
11170                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11171                    ) =>
11172            {
11173                let enabled = s.eq_ignore_ascii_case("enable");
11174                self.advance(); // ENABLE/DISABLE
11175                self.expect_row_level_security()?;
11176                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11177                    enabled: Some(enabled),
11178                    force: None,
11179                }])
11180            }
11181            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11182                self.advance();
11183                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11184                // {INDEX|KEY} [name] (cols)`, which every ORM migration
11185                // emits. The same grammar CREATE TABLE already accepts
11186                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11187                // through the SAME parser — an ALTER-only copy would be a
11188                // second place for the two to drift.
11189                if self.peek_mysql_inline_key_start() {
11190                    return Ok(match self.parse_mysql_inline_key()? {
11191                        Some(c) => {
11192                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11193                        }
11194                        // FULLTEXT / SPATIAL parse and are accepted as a
11195                        // no-op here exactly as they are inline.
11196                        None => Vec::new(),
11197                    });
11198                }
11199                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11200                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11201                // PRIMARY KEY this way; mysqldump emits both.
11202                // Peek-only dispatch (no advance) — `advance()`
11203                // destructively replaces consumed tokens with Eof,
11204                // so saved-pos restore would land on Eofs.
11205                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11206                {
11207                    // The next-but-one ident is the constraint
11208                    // name; the one after THAT is the kind.
11209                    let kind_pos = self.pos + 2;
11210                    let kind = self.tokens.get(kind_pos).cloned();
11211                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11212                    {
11213                        let fk = self.parse_table_level_fk()?;
11214                        return Ok(alloc::vec![
11215                            crate::ast::AlterTableTarget::AddForeignKey(fk)
11216                        ]);
11217                    }
11218                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11219                    {
11220                        self.advance(); // CONSTRAINT
11221                        // v7.39 (read01 round 48) — keep the name; the engine
11222                        // stores it now instead of dropping it on the floor.
11223                        let con_name = self.expect_ident_like()?;
11224                        self.advance(); // PRIMARY
11225                        self.expect_keyword_ident("key")?;
11226                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11227                        // v7.39 (round 711) — the ALTER form carries the
11228                        // timing too (pg_dump writes it here).
11229                        let (deferrable, initially_deferred) =
11230                            self.consume_deferrable_clauses_timed()?;
11231                        return Ok(alloc::vec![
11232                            crate::ast::AlterTableTarget::AddTableConstraint(
11233                                crate::ast::TableConstraint::PrimaryKey {
11234                                    name: Some(con_name),
11235                                    columns: cols,
11236                                    deferrable,
11237                                    initially_deferred,
11238                                }
11239                            )
11240                        ]);
11241                    }
11242                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11243                    {
11244                        self.advance(); // CONSTRAINT
11245                        // v7.39 (read01 round 48) — keep the name.
11246                        let con_name = self.expect_ident_like()?;
11247                        // v7.22 (mailrs round-13 gap 6) — delegate so
11248                        // the optional `NULLS [NOT] DISTINCT` modifier
11249                        // parses here too (pg_dump emits the ALTER
11250                        // form; semantics enforced by the engine
11251                        // since v7.13).
11252                        let mut uc = self.parse_table_level_unique()?;
11253                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11254                            *name = Some(con_name);
11255                        }
11256                        return Ok(alloc::vec![
11257                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11258                        ]);
11259                    }
11260                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11261                    {
11262                        self.advance(); // CONSTRAINT
11263                        // v7.39 (read01 round 48) — keep the name.
11264                        let con_name = self.expect_ident_like()?;
11265                        self.advance(); // CHECK
11266                        if !matches!(self.peek(), Token::LParen) {
11267                            return Err(self.err(alloc::format!(
11268                                "expected '(' after CHECK, got {:?}", self.peek()
11269                            )));
11270                        }
11271                        self.advance();
11272                        let expr = self.parse_expr(0)?;
11273                        if matches!(self.peek(), Token::RParen) {
11274                            self.advance();
11275                        }
11276                        let not_valid = self.parse_not_valid_suffix();
11277                        return Ok(alloc::vec![
11278                            crate::ast::AlterTableTarget::AddTableConstraint(
11279                                crate::ast::TableConstraint::Check {
11280                                    name: Some(con_name),
11281                                    expr,
11282                                    not_valid,
11283                                }
11284                            )
11285                        ]);
11286                    }
11287                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11288                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11289                    // exclusion constraints via this ALTER form.
11290                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11291                    {
11292                        self.advance(); // CONSTRAINT
11293                        let con_name = self.expect_ident_like()?;
11294                        let mut ex = self.parse_table_level_exclude()?;
11295                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11296                            *name = Some(con_name);
11297                        }
11298                        return Ok(alloc::vec![
11299                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11300                        ]);
11301                    }
11302                    // Unknown kind — fall through to FK path which
11303                    // produces a descriptive parse error.
11304                }
11305                let is_fk = matches!(
11306                    self.peek(),
11307                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11308                        || s.eq_ignore_ascii_case("foreign")
11309                );
11310                if is_fk {
11311                    let fk = self.parse_table_level_fk()?;
11312                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11313                }
11314                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11315                // (no CONSTRAINT prefix) — same dispatch.
11316                match self.peek().clone() {
11317                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11318                        self.advance();
11319                        self.expect_keyword_ident("key")?;
11320                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11321                        let (deferrable, initially_deferred) =
11322                            self.consume_deferrable_clauses_timed()?;
11323                        return Ok(alloc::vec![
11324                            crate::ast::AlterTableTarget::AddTableConstraint(
11325                                crate::ast::TableConstraint::PrimaryKey {
11326                                    name: None,
11327                                    columns: cols,
11328                                    deferrable,
11329                                    initially_deferred,
11330                                }
11331                            )
11332                        ]);
11333                    }
11334                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11335                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
11336                        let uc = self.parse_table_level_unique()?;
11337                        return Ok(alloc::vec![
11338                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11339                        ]);
11340                    }
11341                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11342                    // prefix). The other three bare forms were here and
11343                    // this one was not, so it fell through to the column
11344                    // path and came back as "unexpected reserved keyword
11345                    // 'check' at start of column definition".
11346                    _ if self.peek_table_level_check_start() => {
11347                        let chk = self.parse_table_level_check()?;
11348                        let not_valid = self.parse_not_valid_suffix();
11349                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11350                            unreachable!("parse_table_level_check returns Check")
11351                        };
11352                        return Ok(alloc::vec![
11353                            crate::ast::AlterTableTarget::AddTableConstraint(
11354                                crate::ast::TableConstraint::Check {
11355                                    name: None,
11356                                    expr,
11357                                    not_valid,
11358                                }
11359                            )
11360                        ]);
11361                    }
11362                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11363                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11364                        let ex = self.parse_table_level_exclude()?;
11365                        return Ok(alloc::vec![
11366                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11367                        ]);
11368                    }
11369                    _ => {}
11370                }
11371                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11372                    self.advance();
11373                }
11374                let mut if_not_exists = false;
11375                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11376                    self.advance();
11377                    if !matches!(self.peek(), Token::Not) {
11378                        return Err(self.err(alloc::format!(
11379                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11380                            self.peek()
11381                        )));
11382                    }
11383                    self.advance();
11384                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11385                        return Err(self.err(alloc::format!(
11386                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11387                            self.peek()
11388                        )));
11389                    }
11390                    self.advance();
11391                    if_not_exists = true;
11392                }
11393                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11394                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11395                // returns ColumnDef + an optional inline FK.
11396                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11397                let col_name = column.name.clone();
11398                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11399                    column,
11400                    if_not_exists,
11401                }];
11402                if let Some(mut fk) = col_level_fk {
11403                    if fk.columns.is_empty() {
11404                        fk.columns.push(col_name);
11405                    }
11406                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11407                }
11408                Ok(out)
11409            }
11410            Token::Drop => {
11411                self.advance();
11412                // v7.13.3 — dispatch on the next token. mailrs round-7
11413                // S8 closed DROP COLUMN; round-6 S7 closed
11414                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11415                // RESTRICT modifiers.
11416                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11417                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11418                let subject = match self.peek() {
11419                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11420                        self.advance();
11421                        "constraint"
11422                    }
11423                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11424                        self.advance();
11425                        "column"
11426                    }
11427                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11428                    // `INDEX` lexes as the reserved Token::Index, so it is
11429                    // unambiguous. `KEY` is a plain ident, and PG allows a
11430                    // column literally named "key", so only read it as the
11431                    // keyword when a name follows it.
11432                    Token::Index => {
11433                        self.advance();
11434                        "index"
11435                    }
11436                    Token::Ident(s)
11437                        if s.eq_ignore_ascii_case("key")
11438                            && matches!(
11439                                self.tokens.get(self.pos + 1),
11440                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11441                            ) =>
11442                    {
11443                        self.advance();
11444                        "index"
11445                    }
11446                    // PG-canonical bare `DROP <col>` without COLUMN
11447                    // keyword is also valid; treat any other ident
11448                    // as the column name.
11449                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11450                    other => {
11451                        return Err(self.err(alloc::format!(
11452                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11453                        )));
11454                    }
11455                };
11456                let mut if_exists = false;
11457                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11458                    let n1 = self.tokens.get(self.pos + 1);
11459                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11460                        self.advance();
11461                        self.advance();
11462                        if_exists = true;
11463                    }
11464                }
11465                let name = self.expect_ident_like()?;
11466                let mut cascade = false;
11467                if matches!(
11468                    self.peek(),
11469                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11470                        || s.eq_ignore_ascii_case("restrict")
11471                ) {
11472                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11473                    {
11474                        cascade = true;
11475                    }
11476                    self.advance();
11477                }
11478                if subject == "index" {
11479                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11480                        name,
11481                        if_exists,
11482                    }])
11483                } else if subject == "constraint" {
11484                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11485                        name,
11486                        if_exists,
11487                    }])
11488                } else {
11489                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11490                        column: name,
11491                        if_exists,
11492                        cascade,
11493                    }])
11494                }
11495            }
11496            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11497                self.advance();
11498                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11499                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11500                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11501                // immediately; accept-and-no-op.
11502                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11503                    self.advance();
11504                    self.consume_until_statement_boundary();
11505                    return Ok(Vec::new());
11506                }
11507                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11508                    self.advance();
11509                }
11510                let col_name = self.expect_ident_like()?;
11511                match self.peek() {
11512                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11513                        self.advance();
11514                    }
11515                    // v7.14.0 — pg_dump emits BIGSERIAL via
11516                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11517                    // nextval('seq')` (the sequence is created
11518                    // separately). SPG's BIGSERIAL already uses
11519                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11520                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11521                    // engine no-ops by consuming the tail.
11522                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11523                        // v7.22 (round-13 T2) — `SET DEFAULT
11524                        // nextval('…')` is how pg_dump spells a
11525                        // SERIAL column (plain integer in CREATE
11526                        // TABLE + this ALTER). It used to be
11527                        // swallowed as a no-op, which silently
11528                        // STRIPPED auto-increment from imported
11529                        // schemas — the first post-import INSERT
11530                        // without an explicit id then violated NOT
11531                        // NULL. Lower it to the auto-increment
11532                        // marker instead.
11533                        let is_default_nextval =
11534                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11535                                && matches!(
11536                                    self.tokens.get(self.pos + 2),
11537                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11538                                );
11539                        if is_default_nextval {
11540                            let seq_name = self.scan_sequence_name_until_boundary();
11541                            return Ok(alloc::vec![
11542                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11543                                    column: col_name,
11544                                    seq_name,
11545                                }
11546                            ]);
11547                        }
11548                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11549                        self.advance(); // consume "set"
11550                        match self.peek().clone() {
11551                            Token::Default => {
11552                                self.advance();
11553                                let default_expr = self.parse_expr(0)?;
11554                                return Ok(alloc::vec![
11555                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11556                                        column: col_name,
11557                                        default_expr,
11558                                    }
11559                                ]);
11560                            }
11561                            Token::Not => {
11562                                self.advance();
11563                                if !matches!(self.peek(), Token::Null) {
11564                                    return Err(self.err(alloc::format!(
11565                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11566                                        self.peek()
11567                                    )));
11568                                }
11569                                self.advance();
11570                                return Ok(alloc::vec![
11571                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11572                                        column: col_name,
11573                                    }
11574                                ]);
11575                            }
11576                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11577                            // stored generated column's expression and
11578                            // recompute existing rows.
11579                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11580                                self.advance(); // EXPRESSION
11581                                if matches!(self.peek(), Token::As) {
11582                                    self.advance();
11583                                }
11584                                let expr = self.parse_expr(0)?;
11585                                return Ok(alloc::vec![
11586                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11587                                        column: col_name,
11588                                        expr,
11589                                    }
11590                                ]);
11591                            }
11592                            other => {
11593                                // Other SET subjects (STATISTICS,
11594                                // STORAGE, COMPRESSION, …) stay no-ops —
11595                                // storage hints with no SPG semantics.
11596                                let _ = other;
11597                                self.consume_until_statement_boundary();
11598                                return Ok(Vec::new());
11599                            }
11600                        }
11601                    }
11602                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11603                        self.advance(); // consume "drop"
11604                        return self.parse_alter_column_drop_tail(col_name);
11605                    }
11606                    Token::Drop => {
11607                        self.advance(); // consume Drop token
11608                        return self.parse_alter_column_drop_tail(col_name);
11609                    }
11610                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11611                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11612                        // GENERATED { ALWAYS | BY DEFAULT } AS
11613                        // IDENTITY ( … )`: pg_dump's spelling for
11614                        // identity columns. Same auto-increment
11615                        // lowering as the nextval default; the
11616                        // sequence options inside the parens are
11617                        // no-ops under SPG's max+1 semantics.
11618                        let is_generated = matches!(
11619                            self.tokens.get(self.pos + 1),
11620                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11621                        );
11622                        if !is_generated {
11623                            return Err(self.err(alloc::format!(
11624                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11625                                self.tokens.get(self.pos + 1)
11626                            )));
11627                        }
11628                        let seq_name = self.scan_sequence_name_until_boundary();
11629                        return Ok(alloc::vec![
11630                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11631                                column: col_name,
11632                                seq_name,
11633                            }
11634                        ]);
11635                    }
11636                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11637                    // column: floor the next allocated value at n (bare
11638                    // RESTART = restart from the start value, 1).
11639                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11640                        self.advance();
11641                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11642                        {
11643                            self.advance();
11644                            let neg = if matches!(self.peek(), Token::Minus) {
11645                                self.advance();
11646                                true
11647                            } else {
11648                                false
11649                            };
11650                            match self.advance() {
11651                                Token::Integer(v) => Some(if neg { -v } else { v }),
11652                                other => {
11653                                    return Err(self.err(alloc::format!(
11654                                        "expected integer after RESTART WITH, got {other:?}"
11655                                    )));
11656                                }
11657                            }
11658                        } else {
11659                            None
11660                        };
11661                        return Ok(alloc::vec![
11662                            crate::ast::AlterTableTarget::AlterColumnRestart {
11663                                column: col_name,
11664                                with,
11665                            }
11666                        ]);
11667                    }
11668                    other => {
11669                        return Err(self.err(alloc::format!(
11670                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11671                        )));
11672                    }
11673                }
11674                // v7.39 (round 713) — the type parser has consumed a
11675                // trailing `COLLATE <name>` since Phase 2.5, and
11676                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11677                // TYPE text COLLATE "C"` parsed clean and changed
11678                // nothing. Keep the clause; the engine re-collates.
11679                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11680                    self.parse_type_with_implied_flags()?;
11681                let collation = if coll_explicit {
11682                    coll_name.map(|n| (coll, n))
11683                } else {
11684                    None
11685                };
11686                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11687                {
11688                    self.advance();
11689                    Some(self.parse_expr(0)?)
11690                } else {
11691                    None
11692                };
11693                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11694                    column: col_name,
11695                    new_type,
11696                    using,
11697                    collation,
11698                }])
11699            }
11700            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11701            // PG also supports `RENAME TO new_table` for table-name
11702            // rename; that surface is deferred (pg_dump never emits
11703            // it). If the first post-RENAME ident is `TO`, the user
11704            // is asking for table rename — error with a clear
11705            // message rather than misparsing `TO` as a column name.
11706            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11707                self.advance();
11708                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11709                // table-name rename (mailrs round-10 A.5 — used
11710                // by migrate-042's `RENAME TO email_contacts`).
11711                // `TO` lexes as Token::To.
11712                if matches!(self.peek(), Token::To)
11713                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11714                {
11715                    self.advance();
11716                    let new = self.expect_ident_like()?;
11717                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11718                        new,
11719                    }]);
11720                }
11721                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11722                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11723                    self.advance();
11724                    let old = self.expect_ident_like()?;
11725                    if matches!(self.peek(), Token::To) {
11726                        self.advance();
11727                    } else {
11728                        self.expect_keyword_ident("to")?;
11729                    }
11730                    let new = self.expect_ident_like()?;
11731                    return Ok(alloc::vec![
11732                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11733                    ]);
11734                }
11735                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11736                    self.advance();
11737                }
11738                let old = self.expect_ident_like()?;
11739                // `TO` is a reserved keyword token; accept both
11740                // Token::To and Token::Ident("to") for consistency.
11741                if matches!(self.peek(), Token::To) {
11742                    self.advance();
11743                } else {
11744                    self.expect_keyword_ident("to")?;
11745                }
11746                let new = self.expect_ident_like()?;
11747                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11748                    old,
11749                    new,
11750                }])
11751            }
11752            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11753            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11754            // every data block with these. Real disable semantics —
11755            // not no-op — because reload correctness assumes the
11756            // triggers don't fire (rows already carry their
11757            // computed values from prod).
11758            Token::Ident(s)
11759                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11760            {
11761                let enabled = s.eq_ignore_ascii_case("enable");
11762                self.advance();
11763                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11764                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11765                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11766                // pg_dump output) — anything else falls through to
11767                // the catch-all error below.
11768                // v7.22 (round-13 T3) — mysqldump wraps every data
11769                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11770                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11771                // maintains indexes incrementally — engine no-op.
11772                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11773                    self.advance();
11774                    return Ok(Vec::new());
11775                }
11776                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11777                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11778                // to gate triggers on session_replication_role; SPG
11779                // has no replica role, so the prefix is consumed and
11780                // treated identically to the plain ENABLE/DISABLE
11781                // TRIGGER form.
11782                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11783                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11784                {
11785                    self.advance();
11786                }
11787                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11788                    return Err(self.err(alloc::format!(
11789                        "expected TRIGGER after {}, got {:?}",
11790                        if enabled { "ENABLE" } else { "DISABLE" },
11791                        self.peek()
11792                    )));
11793                }
11794                self.advance();
11795                // `ALL` lexes as Token::All (reserved); also
11796                // accept Token::Ident("all") for symmetry.
11797                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11798                // TRIGGER selectors. USER (= all user triggers) is
11799                // semantically ALL here; REPLICA / ALWAYS gate on
11800                // session_replication_role which SPG doesn't track.
11801                // All map to TriggerSelector::All.
11802                let which = if matches!(self.peek(), Token::All)
11803                    || matches!(self.peek(), Token::Ident(s)
11804                        if s.eq_ignore_ascii_case("all")
11805                            || s.eq_ignore_ascii_case("user")
11806                            || s.eq_ignore_ascii_case("replica")
11807                            || s.eq_ignore_ascii_case("always"))
11808                {
11809                    self.advance();
11810                    crate::ast::TriggerSelector::All
11811                } else {
11812                    let name = self.expect_ident_like()?;
11813                    crate::ast::TriggerSelector::Named(name)
11814                };
11815                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11816                    which,
11817                    enabled,
11818                }])
11819            }
11820            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11821            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11822                self.advance();
11823                if !matches!(self.peek(), Token::Partition)
11824                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11825                        if s.eq_ignore_ascii_case("partition"))
11826                {
11827                    return Err(self.err(alloc::format!(
11828                        "expected PARTITION after ATTACH, got {:?}",
11829                        self.peek()
11830                    )));
11831                }
11832                self.advance();
11833                let child = self.expect_ident_like()?;
11834                let bounds = self.parse_partition_bounds_tail()?;
11835                Ok(alloc::vec![
11836                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
11837                ])
11838            }
11839            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
11840            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
11841                self.advance();
11842                if !matches!(self.peek(), Token::Partition)
11843                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
11844                        if s.eq_ignore_ascii_case("partition"))
11845                {
11846                    return Err(self.err(alloc::format!(
11847                        "expected PARTITION after DETACH, got {:?}",
11848                        self.peek()
11849                    )));
11850                }
11851                self.advance();
11852                let child = self.expect_ident_like()?;
11853                let mut concurrently = false;
11854                let mut finalize = false;
11855                loop {
11856                    match self.peek().clone() {
11857                        Token::Ident(s) | Token::QuotedIdent(s)
11858                            if s.eq_ignore_ascii_case("concurrently") =>
11859                        {
11860                            self.advance();
11861                            concurrently = true;
11862                        }
11863                        Token::Ident(s) | Token::QuotedIdent(s)
11864                            if s.eq_ignore_ascii_case("finalize") =>
11865                        {
11866                            self.advance();
11867                            finalize = true;
11868                        }
11869                        _ => break,
11870                    }
11871                }
11872                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
11873                    child,
11874                    concurrently,
11875                    finalize,
11876                }])
11877            }
11878            other => Err(self.err(alloc::format!(
11879                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
11880            ))),
11881        }
11882    }
11883
11884    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
11885    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
11886    /// TABLE … ATTACH PARTITION. Shares the same grammar as
11887    /// `parse_partition_of_tail`'s bounds branch.
11888    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
11889    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
11890    /// lowering each to the respective AlterTableTarget. Any
11891    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
11892    /// no-op via consume_until_statement_boundary.
11893    fn parse_alter_column_drop_tail(
11894        &mut self,
11895        col_name: String,
11896    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11897        match self.peek().clone() {
11898            Token::Default => {
11899                self.advance();
11900                Ok(alloc::vec![
11901                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
11902                ])
11903            }
11904            Token::Not => {
11905                self.advance();
11906                if !matches!(self.peek(), Token::Null) {
11907                    return Err(self.err(alloc::format!(
11908                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
11909                        self.peek()
11910                    )));
11911                }
11912                self.advance();
11913                Ok(alloc::vec![
11914                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
11915                ])
11916            }
11917            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
11918            // generated column into a plain column.
11919            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11920                self.advance();
11921                // v7.39 (round 187, U10) — IF EXISTS was consumed but
11922                // dropped, so the engine still errored on a plain
11923                // column; PG's semantics are NOTICE + skip.
11924                let mut if_exists = false;
11925                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11926                    self.advance();
11927                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11928                        self.advance();
11929                        if_exists = true;
11930                    }
11931                }
11932                Ok(alloc::vec![
11933                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
11934                        column: col_name,
11935                        if_exists,
11936                    }
11937                ])
11938            }
11939            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
11940            // identity column into a plain column.
11941            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
11942                self.advance();
11943                let mut if_exists = false;
11944                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11945                    self.advance();
11946                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
11947                        self.advance();
11948                        if_exists = true;
11949                    }
11950                }
11951                Ok(alloc::vec![
11952                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
11953                        column: col_name,
11954                        if_exists,
11955                    }
11956                ])
11957            }
11958            _ => {
11959                self.consume_until_statement_boundary();
11960                Ok(Vec::new())
11961            }
11962        }
11963    }
11964
11965    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
11966    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
11967    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
11968    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
11969    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
11970        let mut opts = crate::ast::CopyOptions::default();
11971        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
11972            return Ok(opts);
11973        }
11974        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
11975            self.advance();
11976        }
11977        if matches!(self.peek(), Token::LParen) {
11978            self.advance();
11979            loop {
11980                self.parse_one_copy_option(&mut opts)?;
11981                match self.peek() {
11982                    Token::Comma => {
11983                        self.advance();
11984                    }
11985                    Token::RParen => {
11986                        self.advance();
11987                        break;
11988                    }
11989                    other => {
11990                        return Err(self.err(alloc::format!(
11991                            "expected ',' or ')' in COPY options, got {other:?}"
11992                        )));
11993                    }
11994                }
11995            }
11996        } else {
11997            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
11998                self.parse_one_copy_option(&mut opts)?;
11999            }
12000        }
12001        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12002            return Err(self.err(alloc::format!(
12003                "unexpected token after COPY options: {:?}",
12004                self.peek()
12005            )));
12006        }
12007        Ok(opts)
12008    }
12009
12010    fn parse_one_copy_option(
12011        &mut self,
12012        opts: &mut crate::ast::CopyOptions,
12013    ) -> Result<(), ParseError> {
12014        use crate::ast::CopyFormat;
12015        // The option keyword. NULL lexes as its own token; the rest are
12016        // bare identifiers.
12017        let kw = match self.advance() {
12018            Token::Null => alloc::string::String::from("NULL"),
12019            Token::Ident(s) => s.to_uppercase(),
12020            other => {
12021                return Err(self.err(alloc::format!(
12022                    "expected a COPY option keyword, got {other:?}"
12023                )));
12024            }
12025        };
12026        match kw.as_str() {
12027            "FORMAT" => {
12028                let fmt = self.expect_ident_like()?;
12029                match fmt.to_ascii_uppercase().as_str() {
12030                    "CSV" => opts.format = CopyFormat::Csv,
12031                    "TEXT" => opts.format = CopyFormat::Text,
12032                    other => {
12033                        return Err(self.err(alloc::format!(
12034                            "COPY format \"{}\" not recognized",
12035                            other.to_ascii_lowercase()
12036                        )));
12037                    }
12038                }
12039            }
12040            // Legacy bare format keywords.
12041            "CSV" => opts.format = CopyFormat::Csv,
12042            "TEXT" => opts.format = CopyFormat::Text,
12043            "HEADER" => {
12044                opts.header = match self.peek() {
12045                    Token::True => {
12046                        self.advance();
12047                        true
12048                    }
12049                    Token::False => {
12050                        self.advance();
12051                        false
12052                    }
12053                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12054                        self.advance();
12055                        true
12056                    }
12057                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12058                        self.advance();
12059                        false
12060                    }
12061                    // Bare HEADER (no boolean) means HEADER true.
12062                    _ => true,
12063                };
12064            }
12065            // r1066 (7.38 S5.1) — pgbench 14+ loads with
12066            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12067            // vacuum bookkeeping on a freshly created/truncated
12068            // table; SPG's per-statement visibility makes it a
12069            // faithful no-op, and rejecting it aborted `pgbench -i`
12070            // against the drop-in. Accept ON/OFF/bare, change nothing.
12071            "FREEZE" => match self.peek() {
12072                Token::True | Token::False => {
12073                    self.advance();
12074                }
12075                Token::Ident(s)
12076                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12077                {
12078                    self.advance();
12079                }
12080                _ => {}
12081            },
12082            "DELIMITER" | "QUOTE" | "ESCAPE" => {
12083                let s = match self.advance() {
12084                    Token::String(s) => s,
12085                    other => {
12086                        return Err(self.err(alloc::format!(
12087                            "COPY {kw} expects a single-character string, got {other:?}"
12088                        )));
12089                    }
12090                };
12091                // v7.39 (round 247) — PG's wording (0A000), keyword in
12092                // lowercase: "COPY delimiter must be a single one-byte
12093                // character".
12094                let one_byte_err = || {
12095                    self.err(alloc::format!(
12096                        "COPY {} must be a single one-byte character",
12097                        kw.to_ascii_lowercase()
12098                    ))
12099                };
12100                let mut chars = s.chars();
12101                let c = chars.next().ok_or_else(one_byte_err)?;
12102                if chars.next().is_some() || c.len_utf8() != 1 {
12103                    return Err(one_byte_err());
12104                }
12105                match kw.as_str() {
12106                    "DELIMITER" => opts.delimiter = Some(c),
12107                    "QUOTE" => opts.quote = Some(c),
12108                    _ => opts.escape = Some(c),
12109                }
12110            }
12111            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12112            "FORCE_QUOTE" => {
12113                if matches!(self.peek(), Token::Star) {
12114                    self.advance();
12115                    opts.force_quote = Some(Vec::new());
12116                } else {
12117                    if !matches!(self.peek(), Token::LParen) {
12118                        return Err(self.err(alloc::format!(
12119                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12120                            self.peek()
12121                        )));
12122                    }
12123                    self.advance();
12124                    let mut cols = Vec::new();
12125                    loop {
12126                        cols.push(self.expect_ident_like()?);
12127                        match self.peek() {
12128                            Token::Comma => {
12129                                self.advance();
12130                            }
12131                            Token::RParen => {
12132                                self.advance();
12133                                break;
12134                            }
12135                            other => {
12136                                return Err(self.err(alloc::format!(
12137                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12138                                )));
12139                            }
12140                        }
12141                    }
12142                    opts.force_quote = Some(cols);
12143                }
12144            }
12145            "NULL" => {
12146                opts.null_str = Some(match self.advance() {
12147                    Token::String(s) => s,
12148                    other => {
12149                        return Err(self.err(alloc::format!(
12150                            "COPY NULL expects a quoted string, got {other:?}"
12151                        )));
12152                    }
12153                });
12154            }
12155            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12156            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12157            // FORCE_NULL too.
12158            "FORCE_NOT_NULL" | "FORCE_NULL" => {
12159                let cols = self.parse_copy_column_list(&kw)?;
12160                if kw == "FORCE_NOT_NULL" {
12161                    opts.force_not_null = Some(cols);
12162                } else {
12163                    opts.force_null = Some(cols);
12164                }
12165            }
12166            other => {
12167                // PG's wording, lowercased option name.
12168                return Err(self.err(alloc::format!(
12169                    "option \"{}\" not recognized",
12170                    other.to_ascii_lowercase()
12171                )));
12172            }
12173        }
12174        Ok(())
12175    }
12176
12177    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12178    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12179    /// is the `*` spelling.
12180    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12181        if matches!(self.peek(), Token::Star) {
12182            self.advance();
12183            return Ok(Vec::new());
12184        }
12185        if !matches!(self.peek(), Token::LParen) {
12186            return Err(self.err(alloc::format!(
12187                "expected '(' or '*' after {kw}, got {:?}",
12188                self.peek()
12189            )));
12190        }
12191        self.advance();
12192        let mut cols = Vec::new();
12193        loop {
12194            cols.push(self.expect_ident_like()?);
12195            match self.peek() {
12196                Token::Comma => {
12197                    self.advance();
12198                }
12199                Token::RParen => {
12200                    self.advance();
12201                    break;
12202                }
12203                other => {
12204                    return Err(self.err(alloc::format!(
12205                        "expected ',' or ')' in {kw} list, got {other:?}"
12206                    )));
12207                }
12208            }
12209        }
12210        Ok(cols)
12211    }
12212
12213    fn parse_partition_bounds_tail(
12214        &mut self,
12215    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12216        use crate::ast::PartitionOfBoundsAst;
12217        match self.peek() {
12218            Token::Default => {
12219                self.advance();
12220                Ok(PartitionOfBoundsAst::Default)
12221            }
12222            Token::For => {
12223                self.advance();
12224                if !matches!(self.peek(), Token::Values) {
12225                    return Err(
12226                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12227                    );
12228                }
12229                self.advance();
12230                let want_with = matches!(
12231                    self.peek(),
12232                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12233                );
12234                if want_with {
12235                    self.advance();
12236                    if !matches!(self.peek(), Token::LParen) {
12237                        return Err(self.err(format!(
12238                            "expected '(' after FOR VALUES WITH, got {:?}",
12239                            self.peek()
12240                        )));
12241                    }
12242                    self.advance();
12243                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12244                    loop {
12245                        let key = self.expect_ident_like()?;
12246                        let n = match self.peek().clone() {
12247                            Token::Integer(v) if u32::try_from(v).is_ok() => {
12248                                self.advance();
12249                                v as u32
12250                            }
12251                            other => {
12252                                return Err(self.err(format!(
12253                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12254                                )));
12255                            }
12256                        };
12257                        match key.to_ascii_uppercase().as_str() {
12258                            "MODULUS" => modulus = Some(n),
12259                            "REMAINDER" => remainder = Some(n),
12260                            other => {
12261                                return Err(self.err(format!(
12262                                    "FOR VALUES WITH: unknown key {other:?}; \
12263                                     expected MODULUS or REMAINDER"
12264                                )));
12265                            }
12266                        }
12267                        match self.peek() {
12268                            Token::Comma => {
12269                                self.advance();
12270                            }
12271                            Token::RParen => {
12272                                self.advance();
12273                                break;
12274                            }
12275                            other => {
12276                                return Err(self.err(format!(
12277                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12278                                )));
12279                            }
12280                        }
12281                    }
12282                    let modulus = modulus
12283                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12284                    let remainder = remainder.ok_or_else(|| {
12285                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12286                    })?;
12287                    if modulus == 0 {
12288                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12289                    }
12290                    if remainder >= modulus {
12291                        return Err(self.err(format!(
12292                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12293                        )));
12294                    }
12295                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12296                }
12297                match self.peek() {
12298                    Token::From => {
12299                        self.advance();
12300                        let lower = Box::new(self.parse_partition_bound_expr()?);
12301                        if !matches!(self.peek(), Token::To) {
12302                            return Err(self.err(format!(
12303                                "expected TO after FROM (...), got {:?}",
12304                                self.peek()
12305                            )));
12306                        }
12307                        self.advance();
12308                        let upper = Box::new(self.parse_partition_bound_expr()?);
12309                        Ok(PartitionOfBoundsAst::Range { lower, upper })
12310                    }
12311                    Token::In => {
12312                        self.advance();
12313                        if !matches!(self.peek(), Token::LParen) {
12314                            return Err(self.err(format!(
12315                                "expected '(' after FOR VALUES IN, got {:?}",
12316                                self.peek()
12317                            )));
12318                        }
12319                        self.advance();
12320                        let mut values = Vec::new();
12321                        loop {
12322                            values.push(self.parse_expr(0)?);
12323                            match self.peek() {
12324                                Token::Comma => {
12325                                    self.advance();
12326                                }
12327                                Token::RParen => {
12328                                    self.advance();
12329                                    break;
12330                                }
12331                                other => {
12332                                    return Err(self.err(format!(
12333                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12334                                    )));
12335                                }
12336                            }
12337                        }
12338                        if values.is_empty() {
12339                            return Err(
12340                                self.err("FOR VALUES IN requires at least one literal".to_string())
12341                            );
12342                        }
12343                        Ok(PartitionOfBoundsAst::List { values })
12344                    }
12345                    other => Err(self.err(format!(
12346                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12347                    ))),
12348                }
12349            }
12350            other => Err(self.err(format!(
12351                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12352            ))),
12353        }
12354    }
12355
12356    /// v7.16.2 — peek for `information_schema.<tbl>` /
12357    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12358    /// three tokens + return a synthetic table name the engine's
12359    /// SELECT path recognises as a virtual view. Returns `None`
12360    /// when the head doesn't look like a meta-qualified name.
12361    /// Used by `parse_table_ref` to bypass the
12362    /// `expect_ident_like` schema-strip for these specific PG
12363    /// meta schemas (mailrs round-10 A.3).
12364    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12365        // Extract the schema name. Must be a plain ident token.
12366        let schema = match self.tokens.get(self.pos) {
12367            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12368            _ => return None,
12369        };
12370        // Dot.
12371        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12372            return None;
12373        }
12374        // The table-side ident may lex as a reserved keyword
12375        // (e.g. `Token::Tables`). Tolerate the common ones via a
12376        // helper that reads the trailing token's underlying name.
12377        let tbl = match self.tokens.get(self.pos + 2)? {
12378            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12379            Token::Tables => "tables".to_string(),
12380            // Other PG meta table names that may collide with
12381            // reserved keywords land here as needed.
12382            _ => return None,
12383        };
12384        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12385        // names so the synthetic name doesn't double-prefix
12386        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12387        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12388            ("__spg_info_", tbl.to_ascii_lowercase())
12389        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12390            // v7.39 (round 541) — only the catalogs SPG actually
12391            // synthesises are rewritten, which is what the BARE path
12392            // has always checked. Anything else keeps its own name and
12393            // takes the ordinary route: `pg_stat_activity` and friends
12394            // resolve through meta_view_result, and a name that is no
12395            // catalog at all gets PG's "relation does not exist"
12396            // instead of a message about a view SPG cannot materialise.
12397            let lowered = tbl.to_ascii_lowercase();
12398            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12399                self.advance(); // schema
12400                self.advance(); // dot
12401                self.advance(); // tbl
12402                return Some((lowered.clone(), lowered));
12403            }
12404            let bare = lowered
12405                .strip_prefix("pg_")
12406                .map(alloc::string::String::from)
12407                .unwrap_or(lowered);
12408            ("__spg_pg_", bare)
12409        } else if schema.eq_ignore_ascii_case("mysql") {
12410            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12411            // (`mysql.user`, `mysql.db`). Same synthetic-name
12412            // shape as pg_catalog.
12413            ("__spg_mysql_", tbl.to_ascii_lowercase())
12414        } else {
12415            return None;
12416        };
12417        self.advance(); // schema
12418        self.advance(); // dot
12419        self.advance(); // tbl
12420        Some((
12421            alloc::format!("{prefix}{normalised}"),
12422            tbl.to_ascii_lowercase(),
12423        ))
12424    }
12425
12426    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12427    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12428    /// implicit front of every search_path, so a bare reference to a
12429    /// known catalog table always means the catalog table. Only the
12430    /// names the engine actually synthesises are recognised — any
12431    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12432    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12433        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12434        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12435        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12436        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12437        // through the meta_view_result path instead, and already resolve
12438        // bare — they must NOT be listed here or the __spg_ rewrite would
12439        // mis-target them.)
12440        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12441        let name = match self.tokens.get(self.pos) {
12442            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12443            _ => return None,
12444        };
12445        // A following dot means this ident is a schema qualifier,
12446        // not a table name — let the qualified path handle it.
12447        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12448            return None;
12449        }
12450        if !PG_META_TABLES.contains(&name.as_str()) {
12451            return None;
12452        }
12453        self.advance();
12454        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12455        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12456    }
12457
12458    /// Consume a bare ident if its lowercase matches `kw`, else err.
12459    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12460    /// Peeks only; the caller advances.
12461    fn peek_keyword_ident(&self, kw: &str) -> bool {
12462        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12463    }
12464
12465    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12466        match self.advance() {
12467            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12468            other => Err(ParseError {
12469                message: format!("expected {kw:?}, got {other:?}"),
12470                token_pos: self.consumed_pos(),
12471            }),
12472        }
12473    }
12474
12475    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12476    /// literal (`'foo'`) — same shape used by CREATE USER for the
12477    /// username slot.
12478    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12479        match self.advance() {
12480            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12481            other => Err(ParseError {
12482                message: format!("expected identifier or string, got {other:?}"),
12483                token_pos: self.consumed_pos(),
12484            }),
12485        }
12486    }
12487
12488    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12489        match self.advance() {
12490            Token::String(s) => Ok(s),
12491            other => Err(ParseError {
12492                message: format!("expected quoted string, got {other:?}"),
12493                token_pos: self.consumed_pos(),
12494            }),
12495        }
12496    }
12497
12498    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12499        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12500        // subqueries recurse through here without passing
12501        // parse_expr; share the same nesting budget.
12502        self.enter_nested()?;
12503        let r = self.parse_select_stmt_inner();
12504        self.nest_depth -= 1;
12505        r
12506    }
12507
12508    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12509        // Caller dispatches on Token::Select; the inner helper handles
12510        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12511        // get a fresh bare-select parse and may not have their own ORDER
12512        // BY / LIMIT.
12513        let mut head = self.parse_bare_select()?;
12514        let into = self.pending_select_into.take();
12515        self.parse_setop_chain_into(&mut head)?;
12516        self.parse_select_tail_into(&mut head)?;
12517        // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12518        // `CREATE TABLE t AS SELECT …`, which is what a comment in
12519        // `ast.rs` has claimed since v7.38 and what only CTAS actually
12520        // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12521        // to the body, as it does in PostgreSQL.
12522        if let Some((name, temporary)) = into {
12523            return Ok(Statement::CreateMaterializedView(
12524                crate::ast::CreateMaterializedViewStatement {
12525                    temporary,
12526                    name,
12527                    if_not_exists: false,
12528                    columns: Vec::new(),
12529                    body: head,
12530                    with_data: true,
12531                    as_plain_table: true,
12532                },
12533            ));
12534        }
12535        Ok(Statement::Select(head))
12536    }
12537
12538    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12539    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12540    /// token), and INTERSECT [ALL] (a bare ident — it was never
12541    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12542    /// tighter than UNION / EXCEPT — the executor folds the chain
12543    /// left-to-right, which is already correct for LEADING
12544    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12545    /// pair nests into that previous peer, so A UNION B INTERSECT C
12546    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12547    /// groups.
12548    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12549        // A parenthesized group arrives with its own (already
12550        // regrouped) unions on `head`; only the pairs THIS chain
12551        // appends participate in the precedence regroup below —
12552        // nesting an outer INTERSECT into a group-internal peer
12553        // would dissolve the explicit grouping.
12554        let boundary = head.unions.len();
12555        loop {
12556            let base = match self.peek() {
12557                Token::Union => UnionKind::Distinct,
12558                Token::Except => UnionKind::Except,
12559                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12560                _ => break,
12561            };
12562            self.advance();
12563            let kind = if matches!(self.peek(), Token::All) {
12564                self.advance();
12565                match base {
12566                    UnionKind::Distinct => UnionKind::All,
12567                    UnionKind::Except => UnionKind::ExceptAll,
12568                    _ => UnionKind::IntersectAll,
12569                }
12570            } else {
12571                base
12572            };
12573            let peer = self.parse_bare_select()?;
12574            head.unions.push((kind, peer));
12575        }
12576        let mut pairs = core::mem::take(&mut head.unions);
12577        let tail = pairs.split_off(boundary);
12578        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12579        for (kind, peer) in tail {
12580            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12581            // An intersect nests into the previous element of THIS
12582            // chain only; with no new previous element it stays at
12583            // the outer level (the left fold applies it to the
12584            // whole head, group included).
12585            match (
12586                is_intersect,
12587                regrouped.len() > boundary,
12588                regrouped.last_mut(),
12589            ) {
12590                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12591                _ => regrouped.push((kind, peer)),
12592            }
12593        }
12594        head.unions = regrouped;
12595        Ok(())
12596    }
12597
12598    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12599    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12600    /// the top-level bare VALUES statement reuses it verbatim.
12601    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12602    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12603    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12604    /// where the grouping-set universe is still in scope.
12605    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12606        if !matches!(self.peek(), Token::Order) {
12607            return Ok(Vec::new());
12608        }
12609        self.advance();
12610        if !self.peek_is_by() {
12611            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12612        }
12613        self.advance();
12614        let mut keys = Vec::new();
12615        loop {
12616            // v7.39 (round 691) — save/restore, the discipline this parser
12617            // already uses around `pending_sample_preds`, so a subquery inside
12618            // a key neither inherits nor leaks the channel.
12619            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12620            let saved_coll = self.order_key_collation.take();
12621            let parsed = self.parse_expr(0);
12622            self.in_order_by_key = saved_flag;
12623            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12624            let expr = parsed?;
12625            let desc = if matches!(self.peek(), Token::Desc) {
12626                self.advance();
12627                true
12628            } else if matches!(self.peek(), Token::Asc) {
12629                self.advance();
12630                false
12631            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12632                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12633                // one ordering per type, so the btree comparison operators map
12634                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12635                // would need a custom operator class — honest error.
12636                self.advance();
12637                match self.advance() {
12638                    Token::Lt | Token::LtEq => false,
12639                    Token::Gt | Token::GtEq => true,
12640                    other => {
12641                        return Err(self.err(alloc::format!(
12642                            "ORDER BY USING supports the btree comparison \
12643                             operators (< <= > >=); got {other:?}"
12644                        )));
12645                    }
12646                }
12647            } else {
12648                false
12649            };
12650            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12651            let nulls_first = self.parse_optional_nulls_placement()?;
12652            keys.push(OrderBy {
12653                expr,
12654                desc,
12655                nulls_first,
12656                collation,
12657            });
12658            if matches!(self.peek(), Token::Comma) {
12659                self.advance();
12660            } else {
12661                break;
12662            }
12663        }
12664        Ok(keys)
12665    }
12666
12667    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12668        // v7.39 (round 135) — a grouping-set query may have already parsed +
12669        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12670        // no ORDER BY token is present, keep that pre-set order_by rather than
12671        // clobbering it with an empty list.
12672        let parsed_keys = self.parse_order_by_keys()?;
12673        head.order_by = if parsed_keys.is_empty() {
12674            core::mem::take(&mut head.order_by)
12675        } else {
12676            parsed_keys
12677        };
12678        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12679        // order. PG's grammar takes a limit clause and an offset clause
12680        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12681        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12682        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12683        // spelling died on `expected end of input, got Limit`.
12684        //
12685        // Each may appear at most once, and LIMIT and FETCH FIRST are
12686        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12687        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12688        // A second one is left unconsumed here, which the caller reports
12689        // as trailing input rather than silently taking the last.
12690        let mut saw_limit = false;
12691        let mut saw_offset = false;
12692        loop {
12693            if !saw_limit && matches!(self.peek(), Token::Limit) {
12694                self.advance();
12695                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12696                // PG synonyms for "no limit". Treat both as None
12697                // (no head.limit set) so the engine's existing
12698                // unlimited-result path takes over. Reject was the
12699                // pre-5.1 behaviour and broke pg_dump-flavoured
12700                // tooling that occasionally emits LIMIT NULL.
12701                if self.consume_limit_unbounded_sentinel() {
12702                    head.limit = None;
12703                } else {
12704                    let first = self.parse_limit_expr("LIMIT")?;
12705                    // MySQL `LIMIT offset, count` — the first number is
12706                    // the offset when a comma follows.
12707                    if matches!(self.peek(), Token::Comma) {
12708                        self.advance();
12709                        let count = self.parse_limit_expr("LIMIT")?;
12710                        head.offset = Some(first);
12711                        saw_offset = true;
12712                        head.limit = Some(count);
12713                    } else {
12714                        head.limit = Some(first);
12715                    }
12716                }
12717                saw_limit = true;
12718                continue;
12719            }
12720            if !saw_offset && matches!(self.peek(), Token::Offset) {
12721                self.advance();
12722                // PG also accepts an optional `ROW` / `ROWS` trailer
12723                // after the offset value (`OFFSET 10 ROWS`). The
12724                // FETCH-FIRST branch below relies on the same.
12725                let off = self.parse_limit_expr("OFFSET")?;
12726                self.consume_optional_rows_keyword();
12727                head.offset = Some(off);
12728                saw_offset = true;
12729                continue;
12730            }
12731            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12732            // the SQL-standard alias for LIMIT. PG accepts both
12733            // spellings interchangeably; pg_dump emits FETCH FIRST in
12734            // newer versions. We map it onto `head.limit` so the
12735            // engine path is unified.
12736            if !saw_limit
12737                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12738                    if s.eq_ignore_ascii_case("fetch"))
12739            {
12740                self.advance(); // FETCH
12741                // `FIRST` or `NEXT` (both legal per SQL standard).
12742                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12743                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12744                {
12745                    self.advance();
12746                }
12747                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12748                // implicit 1 — but we always consume one if present).
12749                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12750                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12751                {
12752                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12753                    crate::ast::LimitExpr::Literal(1)
12754                } else {
12755                    self.parse_limit_expr("FETCH FIRST")?
12756                };
12757                // Eat `ROW` / `ROWS` if not already consumed above.
12758                self.consume_optional_rows_keyword();
12759                // Optional `ONLY` (the spec form) — or the SQL:2008
12760                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12761                // now honours WITH TIES by extending past the LIMIT
12762                // truncation point through every row that shares the
12763                // last-kept row's ORDER BY key.
12764                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12765                    if s.eq_ignore_ascii_case("only"))
12766                {
12767                    self.advance();
12768                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12769                    if s.eq_ignore_ascii_case("with"))
12770                {
12771                    self.advance(); // WITH
12772                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12773                        if s.eq_ignore_ascii_case("ties"))
12774                    {
12775                        self.advance();
12776                        head.limit_with_ties = true;
12777                    }
12778                }
12779                head.limit = Some(count);
12780                saw_limit = true;
12781                continue;
12782            }
12783            break;
12784        }
12785        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12786        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12787        //       [ OF table_name [, …] ]
12788        //       [ NOWAIT | SKIP LOCKED ]
12789        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12790        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12791        // SELECT already returns a consistent snapshot — so these
12792        // are accept-and-discard: the parser absorbs them so
12793        // mailrs / Rails / Django code paths that emit `SELECT
12794        // … FOR UPDATE` for advisory pessimistic locking load
12795        // without a parser error. The on-disk locking model is
12796        // unchanged; callers that rely on FOR UPDATE for read-
12797        // through-write ordering still get the right answer
12798        // because SPG serialises writes anyway.
12799        head.locking = self
12800            .consume_optional_for_lock_clauses()
12801            .map(alloc::boxed::Box::new);
12802        Ok(())
12803    }
12804
12805    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12806    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12807    /// LOCKED ]` trailers. Each clause is fully accepted and
12808    /// discarded — SPG's single-writer model already satisfies the
12809    /// callers' implicit ordering requirement. Stops at the first
12810    /// token that isn't `FOR`.
12811    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12812        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12813        // not discarded. PG keeps the strongest of several clauses; the
12814        // policy of the last one wins, which is what this loop records.
12815        let mut seen: Option<crate::ast::LockingClause> = None;
12816        while matches!(self.peek(), Token::For) {
12817            // v7.37.14 (A2.5-stub) — record that this query asked
12818            // for a row lock the parser is about to silently
12819            // discard. Operators surface the count via
12820            // `spg_sql::silent_for_update_count()` so they can
12821            // gauge how much of the workload depends on advisory
12822            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
12823            // before v7.37.15's per-row tuple locking lands.
12824            crate::record_silent_for_update_clause();
12825            self.advance(); // FOR
12826            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
12827            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
12828            let mut no_key = false;
12829            let mut key = false;
12830            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12831                if s.eq_ignore_ascii_case("no"))
12832            {
12833                self.advance(); // NO
12834                no_key = true;
12835                // The next ident should be KEY but be generous;
12836                // anything followed by UPDATE/SHARE is accepted.
12837                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12838                    if s.eq_ignore_ascii_case("key"))
12839                {
12840                    self.advance(); // KEY
12841                }
12842            }
12843            // `KEY` prefix (PG `FOR KEY SHARE`).
12844            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12845                if s.eq_ignore_ascii_case("key"))
12846            {
12847                self.advance(); // KEY
12848                key = true;
12849            }
12850            // Lock-strength keyword: UPDATE / SHARE. Required, but
12851            // we're lenient — an unexpected token here just bails
12852            // (we already consumed FOR; caller's downstream
12853            // dispatch will error if anything actually depends on
12854            // the trailing tokens).
12855            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12856                if s.eq_ignore_ascii_case("update"));
12857            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12858                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
12859            {
12860                self.advance();
12861                use crate::ast::LockStrength as LS;
12862                let strength = match (is_update, no_key, key) {
12863                    (true, true, _) => LS::NoKeyUpdate,
12864                    (true, _, _) => LS::Update,
12865                    (false, _, true) => LS::KeyShare,
12866                    (false, _, _) => LS::Share,
12867                };
12868                seen = Some(crate::ast::LockingClause {
12869                    strength,
12870                    of_tables: alloc::vec::Vec::new(),
12871                    policy: crate::ast::LockWait::Wait,
12872                });
12873            } else {
12874                // FOR by itself (or `FOR KEY` with nothing after) —
12875                // give up on the lock-clause path. We've already
12876                // advanced past FOR; further attempts to parse
12877                // here would clobber state.
12878                return seen;
12879            }
12880            // Optional `OF tbl[, tbl …]`. mailrs emits this when
12881            // joining and locking only a subset of tables.
12882            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12883                if s.eq_ignore_ascii_case("of"))
12884            {
12885                self.advance(); // OF
12886                #[allow(clippy::while_let_loop)]
12887                loop {
12888                    match self.peek() {
12889                        Token::Ident(_) | Token::QuotedIdent(_) => {
12890                            // v7.39 (round 294) — the name is CAPTURED now: PG
12891                            // validates it against the FROM clause, and an
12892                            // uncaptured list silently means "lock everything".
12893                            let mut nm = match self.advance() {
12894                                Token::Ident(n) | Token::QuotedIdent(n) => n,
12895                                _ => alloc::string::String::new(),
12896                            };
12897                            // Optional schema-qualified `schema.table`.
12898                            if matches!(self.peek(), Token::Dot) {
12899                                self.advance();
12900                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
12901                                {
12902                                    self.advance();
12903                                    nm = n;
12904                                }
12905                            }
12906                            if let Some(c) = seen.as_mut() {
12907                                c.of_tables.push(nm);
12908                            }
12909                        }
12910                        _ => break,
12911                    }
12912                    if matches!(self.peek(), Token::Comma) {
12913                        self.advance();
12914                    } else {
12915                        break;
12916                    }
12917                }
12918            }
12919            // Optional `NOWAIT` | `SKIP LOCKED`.
12920            match self.peek().clone() {
12921                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
12922                    self.advance();
12923                    if let Some(c) = seen.as_mut() {
12924                        c.policy = crate::ast::LockWait::NoWait;
12925                    }
12926                }
12927                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
12928                    self.advance(); // SKIP
12929                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12930                        if s.eq_ignore_ascii_case("locked"))
12931                    {
12932                        self.advance(); // LOCKED
12933                        if let Some(c) = seen.as_mut() {
12934                            c.policy = crate::ast::LockWait::SkipLocked;
12935                        }
12936                    }
12937                }
12938                _ => {}
12939            }
12940            // Loop: PG allows multiple FOR clauses chained.
12941        }
12942        seen
12943    }
12944
12945    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
12946    /// Bind value gets resolved during prepared-statement Execute;
12947    /// the Pratt expression parser would over-accept here (e.g.
12948    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
12949    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
12950    /// sentinel tokens (PG synonyms for "no limit"). Returns true
12951    /// when one was consumed; caller skips the regular
12952    /// limit-value parse and leaves `head.limit` at None.
12953    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
12954        if matches!(self.peek(), Token::Null) {
12955            self.advance();
12956            return true;
12957        }
12958        if matches!(self.peek(), Token::All) {
12959            self.advance();
12960            return true;
12961        }
12962        false
12963    }
12964
12965    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
12966    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
12967    /// SQL-standard shape. No-op when missing.
12968    fn consume_optional_rows_keyword(&mut self) {
12969        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12970            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12971        {
12972            self.advance();
12973        }
12974    }
12975
12976    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
12977    ///
12978    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
12979    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
12980    /// constant, which is why that spelling keeps the token path below.
12981    ///
12982    /// Constants are folded here rather than carried into the tree: the
12983    /// 15+ execution paths that read the row count go through
12984    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
12985    /// means "no limit". A clause the engine could not resolve would
12986    /// therefore return the WHOLE table instead of failing. Folding at
12987    /// parse time keeps that impossible; a non-constant clause is still
12988    /// a clean error (recorded residual — closing it wants a resolution
12989    /// pre-pass on the simple-query path, where `substitute_placeholders`
12990    /// does not run).
12991    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
12992        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
12993        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
12994        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
12995        // ONLY` both work (its grammar takes a c_expr). Both measured
12996        // against PG 18.4 in round 305.
12997        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
12998            return self.parse_limit_constant(label);
12999        }
13000        // One pass, no rewind: `advance()` takes each token by
13001        // `mem::replace`, so a consumed token reads back as Eof and this
13002        // parser cannot backtrack. Everything — bare literal included —
13003        // is therefore folded from the parsed expression rather than
13004        // re-read from the token stream.
13005        let start = self.pos;
13006        let e = self.parse_expr(0)?;
13007        if let crate::ast::Expr::Placeholder(n) = e {
13008            return Ok(crate::ast::LimitExpr::Placeholder(n));
13009        }
13010        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13011        match fold_limit_constant(&e) {
13012            Some(Ok(v)) if v < 0 => Err(ParseError {
13013                message: alloc::format!("{neg_label} must not be negative"),
13014                token_pos: start,
13015            }),
13016            Some(Ok(v)) => u32::try_from(v)
13017                .map(crate::ast::LimitExpr::Literal)
13018                .map_err(|_| ParseError {
13019                    message: alloc::format!("{label} value too large: {v}"),
13020                    token_pos: start,
13021                }),
13022            Some(Err(message)) => Err(ParseError {
13023                message: message.replace("{L}", neg_label),
13024                token_pos: start,
13025            }),
13026            // v7.39 (round 305, V23) — not foldable at parse time
13027            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
13028            // expression; the engine evaluates it once before dispatch.
13029            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
13030        }
13031    }
13032
13033    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13034        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
13035        // coercion rules, not just an integer token: a NUMERIC rounds half
13036        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13037        // refused with PG's wording ("LIMIT must not be negative", 2201W /
13038        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13039        // content, failing as an input-syntax error on the value. General
13040        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13041        // they need an Expr-carrying LimitExpr variant.
13042        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13043        let err_at = |message: alloc::string::String, pos: usize| ParseError {
13044            message,
13045            token_pos: pos,
13046        };
13047        match self.advance() {
13048            Token::Integer(n) if n >= 0 => u32::try_from(n)
13049                .map(crate::ast::LimitExpr::Literal)
13050                .map_err(|_| ParseError {
13051                    message: alloc::format!("{label} value too large: {n}"),
13052                    token_pos: self.consumed_pos(),
13053                }),
13054            Token::Integer(_) => Err(err_at(
13055                alloc::format!("{neg_label} must not be negative"),
13056                self.pos.saturating_sub(1),
13057            )),
13058            Token::Numeric(t) => {
13059                let pos = self.pos.saturating_sub(1);
13060                let v: f64 = t.parse().map_err(|_| {
13061                    err_at(
13062                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13063                        pos,
13064                    )
13065                })?;
13066                if v < 0.0 {
13067                    return Err(err_at(
13068                        alloc::format!("{neg_label} must not be negative"),
13069                        pos,
13070                    ));
13071                }
13072                // Round half away from zero — PG's numeric→bigint cast.
13073                // (no_std: no f64::round; v is non-negative, so truncating
13074                // v + 0.5 is the same thing.)
13075                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13076                let rounded = (v + 0.5) as u64;
13077                u32::try_from(rounded)
13078                    .map(crate::ast::LimitExpr::Literal)
13079                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13080            }
13081            Token::Minus => {
13082                let pos = self.pos.saturating_sub(1);
13083                match self.peek() {
13084                    Token::Integer(_) | Token::Numeric(_) => {
13085                        self.advance();
13086                        Err(err_at(
13087                            alloc::format!("{neg_label} must not be negative"),
13088                            pos,
13089                        ))
13090                    }
13091                    other => Err(err_at(
13092                        alloc::format!(
13093                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13094                        ),
13095                        pos,
13096                    )),
13097                }
13098            }
13099            Token::String(t) => {
13100                let pos = self.pos.saturating_sub(1);
13101                match t.trim().parse::<i64>() {
13102                    Ok(n) if n < 0 => Err(err_at(
13103                        alloc::format!("{neg_label} must not be negative"),
13104                        pos,
13105                    )),
13106                    Ok(n) => u32::try_from(n)
13107                        .map(crate::ast::LimitExpr::Literal)
13108                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13109                    Err(_) => Err(err_at(
13110                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13111                        pos,
13112                    )),
13113                }
13114            }
13115            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13116            other => Err(ParseError {
13117                message: alloc::format!(
13118                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13119                ),
13120                token_pos: self.consumed_pos(),
13121            }),
13122        }
13123    }
13124
13125    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13126    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13127    /// `unions` empty and `order_by` / `limit` `None`; the top-level
13128    /// `parse_select_stmt` is responsible for filling those in.
13129    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13130    /// call in the expression tree to the per-set integer bitmask
13131    /// (PG semantics: one bit per argument, MSB first; 1 = the key
13132    /// is dropped in this grouping set). Runs during the ROLLUP /
13133    /// CUBE / GROUPING SETS expansion, where the set is known.
13134    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13135    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13136    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13137        if let Expr::FunctionCall { name, .. } = expr
13138            && name.eq_ignore_ascii_case("grouping")
13139        {
13140            if !out.iter().any(|e| e == expr) {
13141                out.push(expr.clone());
13142            }
13143            return;
13144        }
13145        match expr {
13146            Expr::Binary { lhs, rhs, .. } => {
13147                Self::collect_grouping_calls(lhs, out);
13148                Self::collect_grouping_calls(rhs, out);
13149            }
13150            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13151                Self::collect_grouping_calls(expr, out)
13152            }
13153            Expr::FunctionCall { args, .. } => {
13154                for a in args {
13155                    Self::collect_grouping_calls(a, out);
13156                }
13157            }
13158            Expr::Case {
13159                operand,
13160                branches,
13161                else_branch,
13162            } => {
13163                if let Some(o) = operand {
13164                    Self::collect_grouping_calls(o, out);
13165                }
13166                for (c, v) in branches {
13167                    Self::collect_grouping_calls(c, out);
13168                    Self::collect_grouping_calls(v, out);
13169                }
13170                if let Some(x) = else_branch {
13171                    Self::collect_grouping_calls(x, out);
13172                }
13173            }
13174            _ => {}
13175        }
13176    }
13177
13178    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13179    /// `grp_exprs[k]` with a reference to the synthetic ordering column
13180    /// `__grp_ord_k` (injected per grouping-set branch).
13181    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13182        if let Expr::FunctionCall { name, .. } = expr
13183            && name.eq_ignore_ascii_case("grouping")
13184        {
13185            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13186                *expr = Expr::Column(crate::ast::ColumnName {
13187                    qualifier: None,
13188                    name: alloc::format!("__grp_ord_{k}"),
13189                });
13190            }
13191            return;
13192        }
13193        match expr {
13194            Expr::Binary { lhs, rhs, .. } => {
13195                Self::rewrite_grouping_to_col(lhs, grp_exprs);
13196                Self::rewrite_grouping_to_col(rhs, grp_exprs);
13197            }
13198            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13199                Self::rewrite_grouping_to_col(expr, grp_exprs)
13200            }
13201            Expr::FunctionCall { args, .. } => {
13202                for a in args {
13203                    Self::rewrite_grouping_to_col(a, grp_exprs);
13204                }
13205            }
13206            Expr::Case {
13207                operand,
13208                branches,
13209                else_branch,
13210            } => {
13211                if let Some(o) = operand {
13212                    Self::rewrite_grouping_to_col(o, grp_exprs);
13213                }
13214                for (c, v) in branches {
13215                    Self::rewrite_grouping_to_col(c, grp_exprs);
13216                    Self::rewrite_grouping_to_col(v, grp_exprs);
13217                }
13218                if let Some(x) = else_branch {
13219                    Self::rewrite_grouping_to_col(x, grp_exprs);
13220                }
13221            }
13222            _ => {}
13223        }
13224    }
13225
13226    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13227    /// as the list of key sets it contributes. A bare expression is one
13228    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13229    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13230    /// the concatenation of its items' sets, where an item is itself an
13231    /// element, a parenthesized key list, or the empty set `()`. A
13232    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13233    /// move together.
13234    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13235        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13236        // ROLLUP ( … ) / CUBE ( … )
13237        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13238            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13239        {
13240            let is_cube = is_kw(self.peek(), "cube");
13241            self.advance(); // ROLLUP / CUBE
13242            self.advance(); // (
13243            let mut units: Vec<Vec<Expr>> = Vec::new();
13244            loop {
13245                if matches!(self.peek(), Token::LParen) {
13246                    // Composite unit: (a, b) rolls up as one.
13247                    self.advance();
13248                    let mut unit = Vec::new();
13249                    if !matches!(self.peek(), Token::RParen) {
13250                        loop {
13251                            unit.push(self.parse_expr(0)?);
13252                            match self.peek() {
13253                                Token::Comma => {
13254                                    self.advance();
13255                                }
13256                                Token::RParen => break,
13257                                other => {
13258                                    return Err(self.err(format!(
13259                                        "expected ',' or ')' in grouping unit, got {other:?}"
13260                                    )));
13261                                }
13262                            }
13263                        }
13264                    }
13265                    self.advance(); // )
13266                    units.push(unit);
13267                } else {
13268                    units.push(alloc::vec![self.parse_expr(0)?]);
13269                }
13270                match self.peek() {
13271                    Token::Comma => {
13272                        self.advance();
13273                    }
13274                    Token::RParen => break,
13275                    other => {
13276                        return Err(self.err(format!(
13277                            "expected ',' or ')' in grouping list, got {other:?}"
13278                        )));
13279                    }
13280                }
13281            }
13282            self.advance(); // )
13283            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13284                units
13285                    .iter()
13286                    .zip(unit_sel.iter())
13287                    .filter(|(_, keep)| **keep)
13288                    .flat_map(|(u, _)| u.iter().cloned())
13289                    .collect()
13290            };
13291            let n = units.len();
13292            if is_cube {
13293                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13294                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13295                    .collect();
13296                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13297                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13298            }
13299            return Ok((0..=n)
13300                .rev()
13301                .map(|keep| {
13302                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13303                    flatten(&sel)
13304                })
13305                .collect());
13306        }
13307        // GROUPING SETS ( item [, item]* )
13308        if is_kw(self.peek(), "grouping")
13309            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13310        {
13311            self.advance(); // GROUPING
13312            self.advance(); // SETS
13313            if !matches!(self.peek(), Token::LParen) {
13314                return Err(self.err(format!(
13315                    "expected '(' after GROUPING SETS, got {:?}",
13316                    self.peek()
13317                )));
13318            }
13319            self.advance(); // outer (
13320            let mut sets: Vec<Vec<Expr>> = Vec::new();
13321            loop {
13322                if matches!(self.peek(), Token::LParen) {
13323                    // A parenthesized key list (or the empty set).
13324                    self.advance();
13325                    let mut set = Vec::new();
13326                    if !matches!(self.peek(), Token::RParen) {
13327                        loop {
13328                            set.push(self.parse_expr(0)?);
13329                            match self.peek() {
13330                                Token::Comma => {
13331                                    self.advance();
13332                                }
13333                                Token::RParen => break,
13334                                other => {
13335                                    return Err(self.err(format!(
13336                                        "expected ',' or ')' in grouping set, got {other:?}"
13337                                    )));
13338                                }
13339                            }
13340                        }
13341                    }
13342                    self.advance(); // )
13343                    sets.push(set);
13344                } else {
13345                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13346                    // bare expression.
13347                    sets.extend(self.parse_grouping_element()?);
13348                }
13349                match self.peek() {
13350                    Token::Comma => {
13351                        self.advance();
13352                    }
13353                    Token::RParen => break,
13354                    other => {
13355                        return Err(self.err(format!(
13356                            "expected ',' or ')' after a grouping set, got {other:?}"
13357                        )));
13358                    }
13359                }
13360            }
13361            self.advance(); // outer )
13362            return Ok(sets);
13363        }
13364        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13365    }
13366
13367    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13368        // v7.38 (read01) — a reference to a key that is dropped in this grouping
13369        // set evaluates to NULL, at any depth. Previously only a *top-level*
13370        // select item equal to a dropped key was nullified, so a key nested in
13371        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13372        // column and failed to resolve against the set's synthetic schema.
13373        if dropped.iter().any(|d| d == expr) {
13374            *expr = Expr::Literal(Literal::Null);
13375            return;
13376        }
13377        if let Expr::FunctionCall { name, args } = expr
13378            && name.eq_ignore_ascii_case("grouping")
13379        {
13380            let mut mask: i64 = 0;
13381            for a in args.iter() {
13382                mask <<= 1;
13383                if dropped.iter().any(|d| d == a) {
13384                    mask |= 1;
13385                }
13386            }
13387            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13388            // literal: a bare integer in a select item is indistinguishable
13389            // from a positional reference once `ORDER BY 1` substitutes the
13390            // item back in, and the round-232 position check then read the
13391            // mask value as an out-of-range position. The cast changes
13392            // nothing semantically (grouping() is integer).
13393            *expr = Expr::Cast {
13394                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13395                target: crate::ast::CastTarget::Int,
13396            };
13397            return;
13398        }
13399        // Generic recursion over the common expression shapes the
13400        // SELECT list uses; anything without child expressions is
13401        // left alone.
13402        match expr {
13403            Expr::FunctionCall { args, .. } => {
13404                for a in args {
13405                    Self::substitute_grouping_calls(a, dropped);
13406                }
13407            }
13408            Expr::Binary { lhs, rhs, .. } => {
13409                Self::substitute_grouping_calls(lhs, dropped);
13410                Self::substitute_grouping_calls(rhs, dropped);
13411            }
13412            Expr::Unary { expr: inner, .. } => {
13413                Self::substitute_grouping_calls(inner, dropped);
13414            }
13415            Expr::Cast { expr: inner, .. } => {
13416                Self::substitute_grouping_calls(inner, dropped);
13417            }
13418            Expr::Case {
13419                operand,
13420                branches,
13421                else_branch,
13422            } => {
13423                if let Some(op) = operand {
13424                    Self::substitute_grouping_calls(op, dropped);
13425                }
13426                for (w, t) in branches {
13427                    Self::substitute_grouping_calls(w, dropped);
13428                    Self::substitute_grouping_calls(t, dropped);
13429                }
13430                if let Some(e) = else_branch {
13431                    Self::substitute_grouping_calls(e, dropped);
13432                }
13433            }
13434            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13435            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13436            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13437            // …` is the canonical rollup-total label idiom).
13438            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13439            Expr::Like { expr, pattern, .. } => {
13440                Self::substitute_grouping_calls(expr, dropped);
13441                Self::substitute_grouping_calls(pattern, dropped);
13442            }
13443            Expr::InList { expr, list, .. } => {
13444                Self::substitute_grouping_calls(expr, dropped);
13445                for item in list {
13446                    Self::substitute_grouping_calls(item, dropped);
13447                }
13448            }
13449            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13450            Expr::Array(items) => {
13451                for item in items {
13452                    Self::substitute_grouping_calls(item, dropped);
13453                }
13454            }
13455            Expr::ArraySubscript { target, index } => {
13456                Self::substitute_grouping_calls(target, dropped);
13457                Self::substitute_grouping_calls(index, dropped);
13458            }
13459            Expr::ArraySlice { target, lo, hi } => {
13460                Self::substitute_grouping_calls(target, dropped);
13461                if let Some(lo) = lo {
13462                    Self::substitute_grouping_calls(lo, dropped);
13463                }
13464                if let Some(hi) = hi {
13465                    Self::substitute_grouping_calls(hi, dropped);
13466                }
13467            }
13468            Expr::AnyAll { expr, array, .. } => {
13469                Self::substitute_grouping_calls(expr, dropped);
13470                Self::substitute_grouping_calls(array, dropped);
13471            }
13472            _ => {}
13473        }
13474    }
13475
13476    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13477        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13478        // group: `( <select chain> )` usable anywhere a query block
13479        // is (head or peer of an outer chain). The group's own
13480        // unions ride the returned SelectStatement; the executor's
13481        // nested-peer recursion runs them.
13482        if matches!(self.peek(), Token::LParen)
13483            && matches!(
13484                self.tokens.get(self.pos + 1),
13485                Some(Token::Select | Token::LParen | Token::Values)
13486            )
13487        {
13488            self.advance(); // (
13489            self.enter_nested()?;
13490            // v7.37 D.20 — a group whose head is a VALUES list:
13491            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13492            // otherwise recurse into a nested SELECT/group head.
13493            let mut head = (if matches!(self.peek(), Token::Values) {
13494                self.advance(); // VALUES
13495                self.parse_values_rows_body()
13496            } else {
13497                self.parse_bare_select()
13498            })
13499            .and_then(|mut h| {
13500                self.parse_setop_chain_into(&mut h)?;
13501                Ok(h)
13502            });
13503            self.nest_depth -= 1;
13504            let mut head = match &mut head {
13505                Ok(h) => core::mem::take(h),
13506                Err(_) => return head,
13507            };
13508            // v7.37.17 (17.6 siblings) — group-internal tail:
13509            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13510            // group head, then wrap the group as a derived table
13511            // (SELECT * FROM (group)) so the outer chain / outer
13512            // tail can't clobber the group's own ordering or limit.
13513            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13514                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13515                    if s.eq_ignore_ascii_case("fetch"));
13516            if has_tail {
13517                self.parse_select_tail_into(&mut head)?;
13518                head = SelectStatement {
13519                    locking: None,
13520                    ctes: Vec::new(),
13521                    distinct: false,
13522                    distinct_on: Vec::new(),
13523                    items: alloc::vec![SelectItem::Wildcard],
13524                    from: Some(FromClause {
13525                        primary: TableRef {
13526                            name: "subquery".to_string(),
13527                            alias: None,
13528                            only: false,
13529                            as_of_segment: None,
13530                            unnest_expr: None,
13531                            unnest_column_aliases: Vec::new(),
13532                            with_ordinality: false,
13533                            generate_series_args: None,
13534                            lateral_subquery: Some(Box::new(head)),
13535                            jsonb_each_text_arg: None,
13536                            table_fn_call: None,
13537                            rows_from: None,
13538                            json_table: None,
13539                            scalar_fn_item: false,
13540                        },
13541                        joins: Vec::new(),
13542                    }),
13543                    where_: None,
13544                    group_by: None,
13545                    group_by_all: false,
13546                    having: None,
13547                    unions: Vec::new(),
13548                    order_by: Vec::new(),
13549                    limit: None,
13550                    offset: None,
13551                    limit_with_ties: false,
13552                    window_check_exprs: Vec::new(),
13553                };
13554            }
13555            if !matches!(self.peek(), Token::RParen) {
13556                return Err(self.err(format!(
13557                    "expected ')' after parenthesized query group, got {:?}",
13558                    self.peek()
13559                )));
13560            }
13561            self.advance();
13562            return Ok(head);
13563        }
13564        // `TABLE name` shorthand as a query block — valid anywhere
13565        // a SELECT head is (set-op peers included).
13566        if matches!(self.peek(), Token::Table)
13567            && matches!(
13568                self.tokens.get(self.pos + 1),
13569                Some(Token::Ident(_) | Token::QuotedIdent(_))
13570            )
13571        {
13572            return self.parse_table_shorthand();
13573        }
13574        if !matches!(self.peek(), Token::Select) {
13575            return Err(self.err(format!(
13576                "expected SELECT to start a query block, got {:?}",
13577                self.peek()
13578            )));
13579        }
13580        self.advance();
13581        let distinct = if matches!(self.peek(), Token::Distinct) {
13582            self.advance();
13583            true
13584        } else {
13585            false
13586        };
13587        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13588        // keep the first row (per ORDER BY) of each group the
13589        // expressions define. Django's .distinct('field') shape.
13590        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13591            self.advance(); // ON
13592            if !matches!(self.peek(), Token::LParen) {
13593                return Err(self.err(format!(
13594                    "expected '(' after DISTINCT ON, got {:?}",
13595                    self.peek()
13596                )));
13597            }
13598            self.advance();
13599            let mut exprs = Vec::new();
13600            loop {
13601                exprs.push(self.parse_expr(0)?);
13602                match self.peek() {
13603                    Token::Comma => {
13604                        self.advance();
13605                    }
13606                    Token::RParen => break,
13607                    other => {
13608                        return Err(self.err(format!(
13609                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13610                        )));
13611                    }
13612                }
13613            }
13614            self.advance(); // )
13615            exprs
13616        } else {
13617            Vec::new()
13618        };
13619        let mut items = self.parse_select_list()?;
13620        // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13621        // of CTAS. It sits exactly here in PG's grammar, right after the
13622        // target list.
13623        //
13624        // A comment in `ast.rs` has said since v7.38 that CTAS and
13625        // `SELECT INTO` lower to the same node. Only CTAS ever did:
13626        // `SELECT i INTO t FROM src` answered `syntax error at or near
13627        // "INTO"`, which the differential found while measuring what
13628        // PostgreSQL tags each of the five materialising forms with. A
13629        // comment describing a capability the code does not have is the
13630        // defect this version has been finding all day, and this is the
13631        // one it found in the parser.
13632        //
13633        // `INTO` is captured rather than consumed here: the name has to
13634        // travel out of a function that returns a `SelectStatement`, and
13635        // the caller lowers the whole thing to the CTAS node.
13636        if matches!(self.peek(), Token::Into) {
13637            self.advance();
13638            // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13639            // the target, not part of its name. SPG has one storage
13640            // class, so `UNLOGGED` is accepted and means nothing, which
13641            // is what it already means on `CREATE TABLE`.
13642            let mut temporary = false;
13643            loop {
13644                match self.peek().clone() {
13645                    Token::Ident(w) | Token::QuotedIdent(w)
13646                        if w.eq_ignore_ascii_case("temp")
13647                            || w.eq_ignore_ascii_case("temporary") =>
13648                    {
13649                        temporary = true;
13650                        self.advance();
13651                    }
13652                    Token::Ident(w) | Token::QuotedIdent(w)
13653                        if w.eq_ignore_ascii_case("unlogged") =>
13654                    {
13655                        self.advance();
13656                    }
13657                    Token::Table => {
13658                        self.advance();
13659                    }
13660                    _ => break,
13661                }
13662            }
13663            let name = match self.peek().clone() {
13664                Token::Ident(w) | Token::QuotedIdent(w) => {
13665                    self.advance();
13666                    w
13667                }
13668                other => {
13669                    return Err(self.err(alloc::format!(
13670                        "expected a table name after SELECT … INTO, got {other:?}"
13671                    )));
13672                }
13673            };
13674            self.pending_select_into = Some((name, temporary));
13675        }
13676        // Scope the TABLESAMPLE lowering channel to this SELECT:
13677        // stash whatever an enclosing select accumulated, collect
13678        // our own FROM's predicates, restore after the combine.
13679        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13680        let mut from = if matches!(self.peek(), Token::From) {
13681            self.advance();
13682            Some(self.parse_from_clause()?)
13683        } else {
13684            None
13685        };
13686        // v7.37 D.22 — a set-returning function in the projection with no FROM
13687        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13688        // rows. Move the first SRF projection item to a FROM-position derived
13689        // table and replace it in the projection with a reference to its output
13690        // column; sibling scalar columns repeat per SRF row. PG names the output
13691        // column after the function (or its AS alias). Reuses the FROM-SRF
13692        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13693        // works via the targetlist-SRF path.
13694        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13695        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13696        // is exactly what the function's own row shape already is. Anywhere else
13697        // (per outer row, or beside other items) it would need a real record-typed
13698        // projection, so it says so rather than answering something else.
13699        if let [
13700            SelectItem::Expr {
13701                expr: Expr::FunctionCall { name, args },
13702                ..
13703            },
13704        ] = items.as_slice()
13705            && name == "__record_expand"
13706        {
13707            let Some(Expr::FunctionCall {
13708                name: inner_name,
13709                args: inner_args,
13710            }) = args.first()
13711            else {
13712                return Err(self.err(
13713                    "(<expr>).* expands a function's record — it needs a function call".into(),
13714                ));
13715            };
13716            if from.is_some() {
13717                return Err(self.err(
13718                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13719                        .into(),
13720                ));
13721            }
13722            let fn_ref = TableRef {
13723                name: inner_name.clone(),
13724                alias: None,
13725                only: false,
13726                as_of_segment: None,
13727                unnest_expr: None,
13728                unnest_column_aliases: Vec::new(),
13729                with_ordinality: false,
13730                generate_series_args: None,
13731                lateral_subquery: None,
13732                jsonb_each_text_arg: None,
13733                table_fn_call: Some(Box::new((
13734                    inner_name.to_ascii_lowercase(),
13735                    inner_args.clone(),
13736                ))),
13737                rows_from: None,
13738                json_table: None,
13739                scalar_fn_item: false,
13740            };
13741            items = alloc::vec![SelectItem::Wildcard];
13742            from = Some(FromClause {
13743                primary: fn_ref,
13744                joins: Vec::new(),
13745            });
13746        }
13747        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13748        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13749        // record's fields takes the catalog. It becomes a LATERAL of the same
13750        // function plus one item per declared column — the machinery rounds 65
13751        // and 69 already built.
13752        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13753        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13754        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13755        // express, since the lifted one becomes a scan and the other would
13756        // expand per its rows (a cross product, not a zip). So when the
13757        // projection holds more than one top-level function call, the lift steps
13758        // aside and the engine's target-list expansion takes the whole list.
13759        let fn_call_items = items
13760            .iter()
13761            .filter(|it| {
13762                matches!(
13763                    it,
13764                    SelectItem::Expr {
13765                        expr: Expr::FunctionCall { .. },
13766                        ..
13767                    }
13768                )
13769            })
13770            .count();
13771        if from.is_none() && fn_call_items <= 1 {
13772            let mut found: Option<(usize, TableRef, String)> = None;
13773            for (i, item) in items.iter().enumerate() {
13774                if let SelectItem::Expr {
13775                    expr: Expr::FunctionCall { name, args },
13776                    alias,
13777                } = item
13778                {
13779                    let lname = name.to_ascii_lowercase();
13780                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13781                    let (unnest, gs) = match lname.as_str() {
13782                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
13783                        "generate_series" if (2..=3).contains(&args.len()) => {
13784                            (None, Some(args.clone()))
13785                        }
13786                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
13787                        // no-FROM projection yields the 1-based subscripts, i.e.
13788                        // generate_series(1, array_length(arr, dim)); an invalid
13789                        // dimension makes array_length NULL → 0 rows, as in PG.
13790                        "generate_subscripts" if args.len() == 2 => (
13791                            None,
13792                            Some(alloc::vec![
13793                                Expr::Literal(Literal::Integer(1)),
13794                                Expr::FunctionCall {
13795                                    name: "array_length".to_string(),
13796                                    args: args.clone(),
13797                                },
13798                            ]),
13799                        ),
13800                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
13801                        // in a no-FROM projection unnest their *_to_array form.
13802                        "string_to_table" | "regexp_split_to_table" => {
13803                            let array_fn = if lname == "string_to_table" {
13804                                "string_to_array"
13805                            } else {
13806                                "regexp_split_to_array"
13807                            };
13808                            (
13809                                Some(Box::new(Expr::FunctionCall {
13810                                    name: array_fn.to_string(),
13811                                    args: args.clone(),
13812                                })),
13813                                None,
13814                            )
13815                        }
13816                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
13817                        // a no-FROM projection expand per element. The scalar form
13818                        // returns the elements as a TEXT array, so unnest over the
13819                        // same call materialises one row each (same rewrite the
13820                        // FROM-clause form uses).
13821                        "jsonb_array_elements"
13822                        | "json_array_elements"
13823                        | "jsonb_array_elements_text"
13824                        | "json_array_elements_text"
13825                            if args.len() == 1 =>
13826                        {
13827                            (
13828                                Some(Box::new(Expr::FunctionCall {
13829                                    name: lname.clone(),
13830                                    args: args.clone(),
13831                                })),
13832                                None,
13833                            )
13834                        }
13835                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
13836                        // in a no-FROM projection expands per match (scalar form
13837                        // returns the matches as a TEXT array → unnest).
13838                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
13839                            Some(Box::new(Expr::FunctionCall {
13840                                name: lname.clone(),
13841                                args: args.clone(),
13842                            })),
13843                            None,
13844                        ),
13845                        _ => continue,
13846                    };
13847                    found = Some((
13848                        i,
13849                        TableRef {
13850                            name: colname.clone(),
13851                            alias: Some(colname.clone()),
13852                            only: false,
13853                            as_of_segment: None,
13854                            unnest_expr: unnest,
13855                            unnest_column_aliases: alloc::vec![colname.clone()],
13856                            with_ordinality: false,
13857                            generate_series_args: gs,
13858                            lateral_subquery: None,
13859                            jsonb_each_text_arg: None,
13860                            table_fn_call: None,
13861                            rows_from: None,
13862                            json_table: None,
13863                            scalar_fn_item: false,
13864                        },
13865                        colname,
13866                    ));
13867                    break;
13868                }
13869            }
13870            if let Some((idx, tref, colname)) = found {
13871                from = Some(FromClause {
13872                    primary: tref,
13873                    joins: Vec::new(),
13874                });
13875                items[idx] = SelectItem::Expr {
13876                    expr: Expr::Column(ColumnName {
13877                        qualifier: None,
13878                        name: colname.clone(),
13879                    }),
13880                    alias: Some(colname),
13881                };
13882            }
13883        }
13884        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
13885        let where_ = if matches!(self.peek(), Token::Where) {
13886            self.advance();
13887            Some(self.parse_expr(0)?)
13888        } else {
13889            None
13890        };
13891        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
13892            Some(match acc {
13893                Some(w) => Expr::Binary {
13894                    lhs: Box::new(pred),
13895                    op: crate::ast::BinOp::And,
13896                    rhs: Box::new(w),
13897                },
13898                None => pred,
13899            })
13900        });
13901        self.pending_sample_preds = enclosing_sample_preds;
13902        let mut group_by_all = false;
13903        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
13904        // share one expansion: `grouping_sets` lists the key subsets
13905        // (first = primary, assigned to stmt.group_by; the rest
13906        // become UNION ALL peers), `grouping_universe` is the full
13907        // key list used to compute each peer's dropped keys.
13908        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
13909        let mut grouping_universe: Vec<Expr> = Vec::new();
13910        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
13911        // A BOOL, not the key list: this frame is the statement parser's, and
13912        // round 430 measured that a `Vec` local here is enough on its own to
13913        // tip the 512 KiB nesting guard. The keys are recoverable from
13914        // `grouping_universe`, which a rollup fills with exactly them.
13915        let mut mysql_rollup = false;
13916        let group_by = if matches!(self.peek(), Token::Group) {
13917            self.advance();
13918            if !self.peek_is_by() {
13919                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
13920            }
13921            self.advance();
13922            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
13923            // every non-aggregate SELECT-list item later.
13924            if matches!(self.peek(), Token::All) {
13925                self.advance();
13926                group_by_all = true;
13927                None
13928            } else {
13929                // v7.39 (round 242) — PG's general grouping-element grammar:
13930                // GROUP BY [DISTINCT] element [, element]*, where an element
13931                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
13932                // SETS (…) — mixed freely. Each element yields a list of
13933                // key sets; the query's grouping sets are the CARTESIAN
13934                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
13935                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
13936                // content. ROLLUP/CUBE members may be composite
13937                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
13938                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
13939                // parser handled only a lone ROLLUP/CUBE/GS as the whole
13940                // clause.
13941                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
13942                    self.advance();
13943                    true
13944                } else {
13945                    false
13946                };
13947                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
13948                loop {
13949                    element_sets.push(self.parse_grouping_element()?);
13950                    if matches!(self.peek(), Token::Comma) {
13951                        self.advance();
13952                    } else {
13953                        break;
13954                    }
13955                }
13956                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
13957                for el in &element_sets {
13958                    let mut next: Vec<Vec<Expr>> = Vec::new();
13959                    for base in &total {
13960                        for set in el {
13961                            let mut merged = base.clone();
13962                            for k in set {
13963                                if !merged.iter().any(|m| m == k) {
13964                                    merged.push(k.clone());
13965                                }
13966                            }
13967                            next.push(merged);
13968                        }
13969                    }
13970                    total = next;
13971                }
13972                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
13973                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
13974                // The keys and the aggregates come out identical; the ROW
13975                // ORDER does not, and that is the part a report depends on.
13976                // MySQL interleaves each group's subtotal right after its
13977                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
13978                // where the union-of-grouping-sets expansion emits every
13979                // leaf first and then every subtotal. MariaDB REFUSES an
13980                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
13981                // order itself — measured on MariaDB 11 and MySQL 9.7, which
13982                // agree on the order and disagree only on whether ORDER BY
13983                // is allowed (MySQL allows it; SPG allows it too, since
13984                // refusing would break the clients that can write it).
13985                if self.mysql_dialect
13986                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
13987                    && matches!(
13988                        self.tokens.get(self.pos + 1),
13989                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
13990                    )
13991                {
13992                    self.advance(); // WITH
13993                    self.advance(); // ROLLUP
13994                    let keys = total.into_iter().next().unwrap_or_default();
13995                    mysql_rollup = true;
13996                    // n+1 prefixes, largest first — the same expansion
13997                    // `ROLLUP (…)` produces.
13998                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
13999                }
14000                if distinct_sets {
14001                    let mut seen: Vec<Vec<String>> = Vec::new();
14002                    total.retain(|set| {
14003                        let mut key: Vec<String> =
14004                            set.iter().map(|e| alloc::format!("{e}")).collect();
14005                        key.sort();
14006                        if seen.contains(&key) {
14007                            false
14008                        } else {
14009                            seen.push(key);
14010                            true
14011                        }
14012                    });
14013                }
14014                if total.len() > 1 {
14015                    let mut universe: Vec<Expr> = Vec::new();
14016                    for set in &total {
14017                        for k in set {
14018                            if !universe.iter().any(|u| u == k) {
14019                                universe.push(k.clone());
14020                            }
14021                        }
14022                    }
14023                    grouping_universe = universe;
14024                    let primary = total[0].clone();
14025                    grouping_sets = total;
14026                    Some(primary)
14027                } else {
14028                    // One set (a plain GROUP BY list, or a single-set
14029                    // spelling like GROUPING SETS ((a, b))). An EMPTY
14030                    // single set — GROUPING SETS (()) — stays
14031                    // `Some(vec![])`: the grand-total group, which must
14032                    // run the aggregate path.
14033                    Some(total.into_iter().next().unwrap_or_default())
14034                }
14035            }
14036        } else {
14037            None
14038        };
14039        let having = if matches!(self.peek(), Token::Having) {
14040            self.advance();
14041            Some(self.parse_expr(0)?)
14042        } else {
14043            None
14044        };
14045        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14046        // OVER w parsed to a marker above; inline each definition
14047        // into the referencing WindowFunction nodes.
14048        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14049        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14050            self.advance();
14051            loop {
14052                let wname = self.expect_ident_like()?;
14053                if !matches!(self.peek(), Token::As) {
14054                    return Err(self.err(format!(
14055                        "expected AS after WINDOW {wname}, got {:?}",
14056                        self.peek()
14057                    )));
14058                }
14059                self.advance();
14060                // v7.39 (round 229) — PG rejects a redefinition outright.
14061                if window_defs
14062                    .iter()
14063                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14064                {
14065                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14066                }
14067                let def = self.parse_over_clause()?;
14068                // A definition may itself copy an earlier one
14069                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14070                // so resolve it against the defs already in scope. Same
14071                // copy rules as an `OVER (w1 …)` in the select list.
14072                let mut probe = Expr::WindowFunction {
14073                    name: String::new(),
14074                    args: Vec::new(),
14075                    partition_by: def.0,
14076                    order_by: def.1,
14077                    frame: def.2,
14078                    null_treatment: crate::ast::NullTreatment::Respect,
14079                    filter: None,
14080                };
14081                Self::substitute_named_windows(&mut probe, &window_defs)
14082                    .map_err(|m| self.err(m))?;
14083                let Expr::WindowFunction {
14084                    partition_by,
14085                    order_by,
14086                    frame,
14087                    ..
14088                } = probe
14089                else {
14090                    unreachable!("probe is a WindowFunction")
14091                };
14092                window_defs.push((wname, (partition_by, order_by, frame)));
14093                if matches!(self.peek(), Token::Comma) {
14094                    self.advance();
14095                    continue;
14096                }
14097                break;
14098            }
14099        }
14100        // v7.39 (round 705) — which definitions did anything reference?
14101        // The ones nothing did used to be dropped here, unexamined, so
14102        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14103        // definition whether referenced or not. Their key expressions ride
14104        // out on the statement for the engine to resolve.
14105        let mut window_refs: Vec<String> = Vec::new();
14106        if !window_defs.is_empty() {
14107            for it in &items {
14108                if let SelectItem::Expr { expr, .. } = it {
14109                    Self::collect_named_window_refs(expr, &mut window_refs);
14110                }
14111            }
14112        }
14113        let window_check_exprs: Vec<Expr> = window_defs
14114            .iter()
14115            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14116            .flat_map(|(_, (partition, order, _))| {
14117                partition
14118                    .iter()
14119                    .cloned()
14120                    .chain(order.iter().map(|(e, _, _)| e.clone()))
14121            })
14122            .collect();
14123        if !window_defs.is_empty()
14124            || items
14125                .iter()
14126                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14127        {
14128            for it in &mut items {
14129                if let SelectItem::Expr { expr, .. } = it {
14130                    Self::substitute_named_windows(expr, &window_defs)
14131                        .map_err(|m| self.err(m))?;
14132                }
14133            }
14134        }
14135        // `GROUP BY 1` — positional keys substitute with the Nth
14136        // select item's expression (same contract ORDER BY has had
14137        // since v6.x). Out-of-range positions error.
14138        let group_by = match group_by {
14139            Some(mut keys) => {
14140                for k in &mut keys {
14141                    if let Expr::Literal(Literal::Integer(n)) = k {
14142                        let idx = *n;
14143                        if idx < 1 || idx as usize > items.len() {
14144                            return Err(self.err(alloc::format!(
14145                                "GROUP BY position {idx} is not in select list"
14146                            )));
14147                        }
14148                        match &items[(idx - 1) as usize] {
14149                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
14150                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14151                                return Err(self.err(alloc::format!(
14152                                    "GROUP BY position {idx} references a wildcard item"
14153                                )));
14154                            }
14155                        }
14156                    }
14157                }
14158                Some(keys)
14159            }
14160            None => None,
14161        };
14162        let mut stmt = SelectStatement {
14163            locking: None,
14164            ctes: Vec::new(),
14165            distinct,
14166            distinct_on,
14167            items,
14168            from,
14169            where_,
14170            group_by,
14171            group_by_all,
14172            having,
14173            unions: Vec::new(),
14174            order_by: Vec::new(),
14175            limit: None,
14176            offset: None,
14177            limit_with_ties: false,
14178            window_check_exprs,
14179        };
14180        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14181        // first set is the primary (already on stmt.group_by); each
14182        // further set becomes a UNION ALL peer with its dropped
14183        // keys (universe minus the set) replaced by NULL literals
14184        // in the peer's items and group_by. PG-legal: non-grouped
14185        // select items must be group keys or aggregates, so a
14186        // dropped key's occurrences in the projection are exactly
14187        // the ones to nullify.
14188        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14189        // over a plain GROUP BY (every argument must be a group key; the
14190        // mask is then 0) and rejects anything else with 42803. SPG's
14191        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14192        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14193        // function `grouping`".
14194        if grouping_sets.len() <= 1 {
14195            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14196            let mut calls: Vec<Expr> = Vec::new();
14197            for item in &stmt.items {
14198                if let SelectItem::Expr { expr, .. } = item {
14199                    Self::collect_grouping_calls(expr, &mut calls);
14200                }
14201            }
14202            if let Some(h) = &stmt.having {
14203                Self::collect_grouping_calls(h, &mut calls);
14204            }
14205            for call in &calls {
14206                let Expr::FunctionCall { args, .. } = call else {
14207                    continue;
14208                };
14209                for a in args {
14210                    if !keys.iter().any(|k| k == a) {
14211                        return Err(self.err(
14212                            "arguments to GROUPING must be grouping expressions of the associated query level"
14213                                .to_string(),
14214                        ));
14215                    }
14216                }
14217            }
14218            if !calls.is_empty() {
14219                for item in &mut stmt.items {
14220                    if let SelectItem::Expr { expr, .. } = item {
14221                        Self::substitute_grouping_calls(expr, &[]);
14222                    }
14223                }
14224                if let Some(h) = &mut stmt.having {
14225                    Self::substitute_grouping_calls(h, &[]);
14226                }
14227            }
14228        }
14229        if grouping_sets.len() > 1 {
14230            // The primary set's own dropped keys nullify in the
14231            // HEAD's projection too (GROUPING SETS's first set may
14232            // omit keys other sets use).
14233            let primary = grouping_sets[0].clone();
14234            let head_dropped: Vec<Expr> = grouping_universe
14235                .iter()
14236                .filter(|u| !primary.iter().any(|k| k == *u))
14237                .cloned()
14238                .collect();
14239            for set in grouping_sets.iter().skip(1) {
14240                let mut peer = stmt.clone();
14241                peer.unions = Vec::new();
14242                let dropped: Vec<&Expr> = grouping_universe
14243                    .iter()
14244                    .filter(|u| !set.iter().any(|k| k == *u))
14245                    .collect();
14246                // Empty set = grand-total group: `Some(vec![])` forces
14247                // the aggregate path (one collapsed row) instead of a
14248                // per-row passthrough. See the primary-set note above.
14249                peer.group_by = Some(set.clone());
14250                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14251                for item in &mut peer.items {
14252                    if let SelectItem::Expr { expr, alias } = item {
14253                        if dropped.iter().any(|d| *d == expr) {
14254                            // v7.39 — keep the dropped key's name on the
14255                            // NULL literal so the UNION output column
14256                            // (and any top-level ORDER BY on it) still
14257                            // resolves.
14258                            if alias.is_none()
14259                                && let Expr::Column(c) = &expr
14260                            {
14261                                *alias = Some(c.name.clone());
14262                            }
14263                            *expr = Expr::Literal(Literal::Null);
14264                        } else {
14265                            Self::substitute_grouping_calls(expr, &dropped_owned);
14266                        }
14267                    }
14268                }
14269                if let Some(h) = &mut peer.having {
14270                    Self::substitute_grouping_calls(h, &dropped_owned);
14271                }
14272                stmt.unions.push((UnionKind::All, peer));
14273            }
14274            for item in &mut stmt.items {
14275                if let SelectItem::Expr { expr, alias } = item {
14276                    if head_dropped.iter().any(|d| d == expr) {
14277                        if alias.is_none()
14278                            && let Expr::Column(c) = &expr
14279                        {
14280                            *alias = Some(c.name.clone());
14281                        }
14282                        *expr = Expr::Literal(Literal::Null);
14283                    } else {
14284                        Self::substitute_grouping_calls(expr, &head_dropped);
14285                    }
14286                }
14287            }
14288            if let Some(h) = &mut stmt.having {
14289                Self::substitute_grouping_calls(h, &head_dropped);
14290            }
14291            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14292            // (while `grouping_universe` / the per-branch sets are in scope). For
14293            // each grouping() call in it, inject a per-branch hidden column
14294            // `__grp_ord_K` carrying that branch's mask into the head + every
14295            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14296            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14297            // from the final output. A standalone grouping-set query has ORDER BY
14298            // (not an explicit set-op) next, so consuming it here is safe.
14299            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14300            // rollup carries the hierarchical order: sort by the grouping
14301            // keys with the rolled-up NULLs last, which is exactly the
14302            // interleaving both oracles emit. A client's own ORDER BY wins,
14303            // which is what MySQL does (MariaDB refuses to let one be
14304            // written at all).
14305            // The synthesised keys have to travel the SAME path a written
14306            // ORDER BY does: the block below is what turns a `grouping()`
14307            // call into the per-branch `__grp_ord_K` column the engine can
14308            // actually sort on. Bypassing it left a bare `grouping(text)`
14309            // for the evaluator to reject.
14310            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14311                self.parse_order_by_keys()?
14312            } else if mysql_rollup {
14313                Self::mysql_rollup_order(&grouping_universe)
14314            } else {
14315                Vec::new()
14316            };
14317            if !synthesised_or_parsed.is_empty() {
14318                let mut order_keys = synthesised_or_parsed;
14319                let mut grp_exprs: Vec<Expr> = Vec::new();
14320                for ob in &order_keys {
14321                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14322                }
14323                for (k, gexpr) in grp_exprs.iter().enumerate() {
14324                    let colname = alloc::format!("__grp_ord_{k}");
14325                    // Head branch (primary set) uses `head_dropped`.
14326                    let mut he = gexpr.clone();
14327                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14328                    stmt.items.push(SelectItem::Expr {
14329                        expr: he,
14330                        alias: Some(colname.clone()),
14331                    });
14332                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14333                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14334                        let set = &grouping_sets[i + 1];
14335                        let dropped: Vec<Expr> = grouping_universe
14336                            .iter()
14337                            .filter(|u| !set.iter().any(|k| k == *u))
14338                            .cloned()
14339                            .collect();
14340                        let mut pe = gexpr.clone();
14341                        Self::substitute_grouping_calls(&mut pe, &dropped);
14342                        peer.items.push(SelectItem::Expr {
14343                            expr: pe,
14344                            alias: Some(colname.clone()),
14345                        });
14346                    }
14347                }
14348                for ob in &mut order_keys {
14349                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14350                }
14351                stmt.order_by = order_keys;
14352            }
14353        }
14354        Ok(stmt)
14355    }
14356
14357    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14358    /// as ORDER BY keys.
14359    ///
14360    /// Per key: the rollup marker, then the key. Sorting on the key alone
14361    /// is not enough, and a table with a NULL in it says why — MariaDB puts
14362    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14363    /// the ROLLUP-introduced NULL last, and both print as NULL.
14364    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14365    /// real group including the data-NULL one, 1 only for the row the
14366    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14367    /// rolls up to NULL|2, a|1, b|3, NULL|6.
14368    ///
14369    /// `#[inline(never)]`: its locals must not join the statement parser's
14370    /// frame, which round 430 measured sitting against the nesting guard.
14371    #[inline(never)]
14372    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14373        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14374        for e in keys {
14375            out.push(OrderBy {
14376                expr: Expr::FunctionCall {
14377                    name: "grouping".into(),
14378                    args: alloc::vec![e.clone()],
14379                },
14380                desc: false,
14381                nulls_first: None,
14382                collation: None,
14383            });
14384            out.push(OrderBy {
14385                expr: e.clone(),
14386                desc: false,
14387                // MySQL orders NULL first on an ascending key.
14388                nulls_first: Some(true),
14389                collation: None,
14390            });
14391        }
14392        out
14393    }
14394
14395    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14396    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14397    #[inline(never)]
14398    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14399        use crate::ast::MaintainKind;
14400        self.skip_paren_option_list();
14401        let kind = match self.peek() {
14402            // `TABLE` and `INDEX` lex as keywords, not identifiers.
14403            Token::Table | Token::Index => {
14404                self.advance();
14405                MaintainKind::ReindexRelation
14406            }
14407            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14408                "index" | "table" => {
14409                    self.advance();
14410                    MaintainKind::ReindexRelation
14411                }
14412                "schema" => {
14413                    self.advance();
14414                    MaintainKind::ReindexSchema
14415                }
14416                "system" | "database" => {
14417                    self.advance();
14418                    MaintainKind::Whole
14419                }
14420                // PG requires the object type; anything else is the
14421                // caller's problem, not something to swallow.
14422                _ => MaintainKind::ReindexRelation,
14423            },
14424            _ => MaintainKind::Whole,
14425        };
14426        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14427        // allows the plain form, so the modifier is recorded rather than
14428        // skipped. It still has no effect on how the reindex runs.
14429        let mut concurrently = false;
14430        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14431            self.advance();
14432            concurrently = true;
14433        }
14434        let target = self.take_optional_maintain_name();
14435        self.consume_until_statement_boundary();
14436        Ok(Statement::Maintain {
14437            kind,
14438            concurrently,
14439            target,
14440        })
14441    }
14442
14443    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14444    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14445    #[inline(never)]
14446    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14447        use crate::ast::MaintainKind;
14448        self.skip_paren_option_list();
14449        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14450            self.advance();
14451        }
14452        let target = self.take_optional_maintain_name();
14453        self.consume_until_statement_boundary();
14454        Ok(Statement::Maintain {
14455            kind: if target.is_some() {
14456                MaintainKind::ClusterRelation
14457            } else {
14458                MaintainKind::Whole
14459            },
14460            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14461            // transaction block quite happily (measured).
14462            concurrently: false,
14463            target,
14464        })
14465    }
14466
14467    /// The next token as a relation / schema name, when there is one.
14468    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14469        match self.peek() {
14470            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14471                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14472                _ => None,
14473            },
14474            _ => None,
14475        }
14476    }
14477
14478    /// A parenthesised option list, absorbed.
14479    fn skip_paren_option_list(&mut self) {
14480        if !matches!(self.peek(), Token::LParen) {
14481            return;
14482        }
14483        let mut depth = 0usize;
14484        loop {
14485            match self.advance() {
14486                Token::LParen => depth += 1,
14487                Token::RParen => {
14488                    depth -= 1;
14489                    if depth == 0 {
14490                        return;
14491                    }
14492                }
14493                Token::Eof => return,
14494                _ => {}
14495            }
14496        }
14497    }
14498
14499    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14500    /// column list.
14501    ///
14502    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14503    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14504    /// / ALL. The three that describe physical storage have no meaning
14505    /// here, so they parse and change nothing rather than making a
14506    /// dump that mentions them fail to load.
14507    ///
14508    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14509    /// parse chain the nesting sentinel is tuned against.
14510    #[inline(never)]
14511    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14512        self.advance(); // LIKE
14513        let source = self.expect_ident_like()?;
14514        let mut options = crate::ast::LikeOptions::default();
14515        loop {
14516            let including = match self.peek() {
14517                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14518                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14519                _ => break,
14520            };
14521            self.advance();
14522            // `ALL` lexes as its own keyword, not an identifier.
14523            let opt = if matches!(self.peek(), Token::All) {
14524                self.advance();
14525                alloc::string::String::from("all")
14526            } else {
14527                self.expect_ident_like()?
14528            };
14529            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14530                o.defaults = on;
14531                o.constraints = on;
14532                o.identity = on;
14533                o.generated = on;
14534                o.indexes = on;
14535                o.comments = on;
14536            };
14537            match opt.to_ascii_lowercase().as_str() {
14538                "all" => set(&mut options, including),
14539                "defaults" => options.defaults = including,
14540                "constraints" => options.constraints = including,
14541                "identity" => options.identity = including,
14542                "generated" => options.generated = including,
14543                "indexes" => options.indexes = including,
14544                "comments" => options.comments = including,
14545                // No storage model to copy into.
14546                "storage" | "statistics" | "compression" => {}
14547                other => {
14548                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14549                }
14550            }
14551        }
14552        Ok(crate::ast::LikeSpec {
14553            source,
14554            at,
14555            options,
14556        })
14557    }
14558
14559    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14560        // Caller already consumed CREATE; we're sitting on TABLE.
14561        debug_assert!(matches!(self.peek(), Token::Table));
14562        self.advance();
14563        let if_not_exists = self.consume_if_not_exists();
14564        let name = self.expect_ident_like()?;
14565        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14566        // child shape has no column list; the child inherits its
14567        // columns from the parent at engine-DDL time. Detect it
14568        // before the `(` requirement below.
14569        if matches!(self.peek(), Token::Partition)
14570            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14571        {
14572            self.advance(); // PARTITION
14573            self.advance(); // of
14574            let partition_of = self.parse_partition_of_tail()?;
14575            return Ok(Statement::CreateTable(CreateTableStatement {
14576                temporary: false,
14577                name,
14578                engine: None,
14579                columns: Vec::new(),
14580                like_specs: Vec::new(),
14581                inherits: Vec::new(),
14582                if_not_exists,
14583                foreign_keys: Vec::new(),
14584                table_constraints: Vec::new(),
14585                partition_by: None,
14586                partition_of: Some(partition_of),
14587            }));
14588        }
14589        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14590        // the materialized-view materialisation path (run the SELECT, infer the
14591        // column types, create + populate the table) but marks the node so the
14592        // executor creates a plain table without a mat-view registry entry.
14593        if matches!(self.peek(), Token::As) {
14594            self.advance();
14595            let body_stmt = self.parse_select_stmt()?;
14596            let Statement::Select(body) = body_stmt else {
14597                return Err(self.err(format!(
14598                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14599                )));
14600            };
14601            let with_data = self.parse_optional_with_data(true)?;
14602            return Ok(Statement::CreateMaterializedView(
14603                crate::ast::CreateMaterializedViewStatement {
14604                    temporary: false,
14605                    name,
14606                    if_not_exists,
14607                    columns: Vec::new(),
14608                    body,
14609                    with_data,
14610                    as_plain_table: true,
14611                },
14612            ));
14613        }
14614        if !matches!(self.peek(), Token::LParen) {
14615            return Err(self.err(format!(
14616                "expected '(' after table name, got {:?}",
14617                self.peek()
14618            )));
14619        }
14620        self.advance();
14621        let mut columns = Vec::new();
14622        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14623        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14624        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14625        loop {
14626            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14627            // column list. It is how a child that adds nothing of its own is
14628            // written, and this loop demanded at least one entry: `syntax
14629            // error at or near ")"`. The child takes the parent's columns,
14630            // which the INHERITS clause already arranges.
14631            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14632                self.advance();
14633                break;
14634            }
14635            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14636            // clauses from column definitions. Constraints start
14637            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14638            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14639            // a column.
14640            if self.peek_table_level_pk_start() {
14641                table_constraints.push(self.parse_table_level_primary_key()?);
14642            } else if matches!(self.peek(), Token::Like) {
14643                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14644                // <opt> ]*`. The source table's shape lives in the catalog,
14645                // so this records the clause and the engine expands it.
14646                like_specs.push(self.parse_create_table_like(columns.len())?);
14647            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14648                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14649                table_constraints.push(self.parse_table_level_exclude()?);
14650            } else if self.peek_table_level_unique_start() {
14651                table_constraints.push(self.parse_table_level_unique()?);
14652            } else if self.peek_table_level_check_start() {
14653                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14654                table_constraints.push(self.parse_table_level_check()?);
14655            } else if self.peek_mysql_inline_key_start() {
14656                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14657                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14658                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14659                // inside the column list. Skip name + paren list;
14660                // for UNIQUE KEY, register as a UC.
14661                if let Some(uc) = self.parse_mysql_inline_key()? {
14662                    table_constraints.push(uc);
14663                }
14664            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14665                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14666                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14667                // CHECK is named, and the named-CONSTRAINT arm used
14668                // to accept FOREIGN KEY only. The name is accepted
14669                // and discarded — same handling as every other SPG
14670                // constraint name.
14671                self.advance(); // CONSTRAINT
14672                // v7.39 (read01 round 48) — the name is kept now: the schema
14673                // stores it, so DROP / RENAME CONSTRAINT can find it.
14674                let con_name = self.expect_ident_like()?;
14675                let mut tc = match kind {
14676                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14677                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14678                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14679                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14680                };
14681                match &mut tc {
14682                    crate::ast::TableConstraint::Check { name, .. }
14683                    | crate::ast::TableConstraint::Unique { name, .. }
14684                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14685                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14686                        *name = Some(con_name);
14687                    }
14688                    _ => {}
14689                }
14690                table_constraints.push(tc);
14691            } else if self.peek_constraint_or_fk_start() {
14692                foreign_keys.push(self.parse_table_level_fk()?);
14693            } else {
14694                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
14695                // v7.13.0 — fold inline UNIQUE / CHECK column
14696                // constraints into table-level entries so the
14697                // engine path stays uniform.
14698                if col.is_unique {
14699                    table_constraints.push(crate::ast::TableConstraint::Unique {
14700                        name: None,
14701                        columns: alloc::vec![col.name.clone()],
14702                        nulls_not_distinct: col.unique_nulls_not_distinct,
14703                        deferrable: col.constraint_deferrable,
14704                        initially_deferred: col.constraint_initially_deferred,
14705                    });
14706                }
14707                if let Some(check_expr) = col.check.clone() {
14708                    table_constraints.push(crate::ast::TableConstraint::Check {
14709                        name: None,
14710                        expr: check_expr,
14711                        not_valid: false,
14712                    });
14713                }
14714                columns.push(col);
14715                if let Some(fk) = col_level_fk {
14716                    foreign_keys.push(fk);
14717                }
14718            }
14719            match self.peek() {
14720                Token::Comma => {
14721                    self.advance();
14722                }
14723                Token::RParen => {
14724                    self.advance();
14725                    break;
14726                }
14727                other => {
14728                    return Err(
14729                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
14730                    );
14731                }
14732            }
14733        }
14734        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
14735        // `CREATE TABLE k (LIKE t)` is a complete definition even though
14736        // nothing is written between the parentheses.
14737        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
14738        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
14739        // empty parentheses were a parse error in their own right — quite apart
14740        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
14741        // SPG does not have (filed separately).
14742        let _ = &like_specs;
14743        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
14744        // It sits between the column list and the MySQL table options,
14745        // and it was a syntax error until this round.
14746        let mut inherits: Vec<String> = Vec::new();
14747        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
14748            if k.eq_ignore_ascii_case("inherits"))
14749        {
14750            self.advance();
14751            if !matches!(self.peek(), Token::LParen) {
14752                return Err(self.err(alloc::format!(
14753                    "expected ( after INHERITS, got {:?}",
14754                    self.peek()
14755                )));
14756            }
14757            self.advance();
14758            loop {
14759                inherits.push(self.expect_ident_like()?);
14760                if matches!(self.peek(), Token::Comma) {
14761                    self.advance();
14762                    continue;
14763                }
14764                break;
14765            }
14766            if !matches!(self.peek(), Token::RParen) {
14767                return Err(self.err(alloc::format!(
14768                    "expected ) closing INHERITS, got {:?}",
14769                    self.peek()
14770                )));
14771            }
14772            self.advance();
14773        }
14774        // v7.14.0 — consume MySQL/MariaDB table options after the
14775        // closing `)`. mysqldump emits things like
14776        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
14777        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
14778        // SPG accepts all forms as no-ops (each option is
14779        // `<ident> [=] <ident-or-string>` separated by whitespace).
14780        let engine = self.consume_mysql_table_options();
14781        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
14782        // SPG has no per-table reloptions, so accept and ignore them so a
14783        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
14784        self.consume_with_reloptions();
14785        // v7.37.6-B — declarative-partition-parent suffix
14786        // (`PARTITION BY RANGE (key_col)`) sits after the column
14787        // list + MySQL table-options. v7.37.6-B only accepts RANGE
14788        // and locks the key column at one ident; the engine then
14789        // verifies the column type is TIMESTAMPTZ.
14790        let partition_by = if matches!(self.peek(), Token::Partition) {
14791            self.advance(); // PARTITION
14792            if !self.peek_is_by() {
14793                return Err(self.err(format!(
14794                    "expected BY after PARTITION, got {:?}",
14795                    self.peek()
14796                )));
14797            }
14798            self.advance();
14799            Some(self.parse_partition_by_tail()?)
14800        } else {
14801            None
14802        };
14803        Ok(Statement::CreateTable(CreateTableStatement {
14804            temporary: false,
14805            name,
14806            engine,
14807            columns,
14808            like_specs,
14809            inherits,
14810            if_not_exists,
14811            foreign_keys,
14812            table_constraints,
14813            partition_by,
14814            partition_of: None,
14815        }))
14816    }
14817
14818    /// v7.37.6-B — case-insensitive ident match helper for the
14819    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
14820    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
14821    /// didn't burn a global keyword slot for each (see the
14822    /// `Token::Partition` doc-comment in `lexer.rs`).
14823    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
14824        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
14825    }
14826
14827    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
14828    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
14829    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
14830        use crate::ast::{PartitionBySpec, PartitionKindAst};
14831        let kind = match self.peek() {
14832            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
14833                self.advance();
14834                PartitionKindAst::Range
14835            }
14836            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
14837                self.advance();
14838                PartitionKindAst::List
14839            }
14840            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
14841                self.advance();
14842                PartitionKindAst::Hash
14843            }
14844            other => {
14845                return Err(self.err(format!(
14846                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
14847                )));
14848            }
14849        };
14850        if !matches!(self.peek(), Token::LParen) {
14851            return Err(self.err(format!(
14852                "expected '(' after PARTITION BY <strategy>, got {:?}",
14853                self.peek()
14854            )));
14855        }
14856        self.advance();
14857        let mut key_columns = Vec::new();
14858        loop {
14859            key_columns.push(self.expect_ident_like()?);
14860            match self.peek() {
14861                Token::Comma => {
14862                    self.advance();
14863                }
14864                Token::RParen => {
14865                    self.advance();
14866                    break;
14867                }
14868                other => {
14869                    return Err(self.err(format!(
14870                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
14871                    )));
14872                }
14873            }
14874        }
14875        if key_columns.is_empty() {
14876            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
14877        }
14878        Ok(PartitionBySpec { kind, key_columns })
14879    }
14880
14881    /// v7.37.6-B — after `PARTITION OF`, expect
14882    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
14883    /// or
14884    ///   <parent> DEFAULT
14885    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
14886        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
14887        let parent_name = self.expect_ident_like()?;
14888        // v7.37.6-B rejects an explicit column list — the child
14889        // inherits from the parent. mailrs round-7 taught us that
14890        // CREATE TABLE-side schema reconciliation hides drift, so
14891        // we surface this as a parse error rather than silently
14892        // ignoring user columns.
14893        if matches!(self.peek(), Token::LParen) {
14894            return Err(self.err(
14895                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
14896                 at v7.37.6-B; the child inherits its columns from the parent"
14897                    .to_string(),
14898            ));
14899        }
14900        let bounds = match self.peek() {
14901            Token::Default => {
14902                self.advance();
14903                PartitionOfBoundsAst::Default
14904            }
14905            Token::For => {
14906                self.advance();
14907                if !matches!(self.peek(), Token::Values) {
14908                    return Err(
14909                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
14910                    );
14911                }
14912                self.advance();
14913                // WITH is not a reserved Token in the lexer — it lexes
14914                // as Token::Ident("with"). Disambiguate manually.
14915                let want_with = matches!(
14916                    self.peek(),
14917                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
14918                );
14919                if want_with {
14920                    self.advance();
14921                    if !matches!(self.peek(), Token::LParen) {
14922                        return Err(self.err(format!(
14923                            "expected '(' after FOR VALUES WITH, got {:?}",
14924                            self.peek()
14925                        )));
14926                    }
14927                    self.advance();
14928                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
14929                    loop {
14930                        let key = self.expect_ident_like()?;
14931                        let n = match self.peek().clone() {
14932                            Token::Integer(v) if u32::try_from(v).is_ok() => {
14933                                self.advance();
14934                                v as u32
14935                            }
14936                            other => {
14937                                return Err(self.err(format!(
14938                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
14939                                )));
14940                            }
14941                        };
14942                        match key.to_ascii_uppercase().as_str() {
14943                            "MODULUS" => modulus = Some(n),
14944                            "REMAINDER" => remainder = Some(n),
14945                            other => {
14946                                return Err(self.err(format!(
14947                                    "FOR VALUES WITH: unknown key {other:?}; \
14948                                     expected MODULUS or REMAINDER"
14949                                )));
14950                            }
14951                        }
14952                        match self.peek() {
14953                            Token::Comma => {
14954                                self.advance();
14955                            }
14956                            Token::RParen => {
14957                                self.advance();
14958                                break;
14959                            }
14960                            other => {
14961                                return Err(self.err(format!(
14962                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
14963                                )));
14964                            }
14965                        }
14966                    }
14967                    let modulus = modulus
14968                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
14969                    let remainder = remainder.ok_or_else(|| {
14970                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
14971                    })?;
14972                    if modulus == 0 {
14973                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
14974                    }
14975                    if remainder >= modulus {
14976                        return Err(self.err(format!(
14977                            "FOR VALUES WITH: REMAINDER ({remainder}) \
14978                             must be < MODULUS ({modulus})"
14979                        )));
14980                    }
14981                    PartitionOfBoundsAst::Hash { modulus, remainder }
14982                } else {
14983                    match self.peek() {
14984                        Token::From => {
14985                            self.advance();
14986                            let lower = Box::new(self.parse_partition_bound_expr()?);
14987                            if !matches!(self.peek(), Token::To) {
14988                                return Err(self.err(format!(
14989                                    "expected TO after FROM (...), got {:?}",
14990                                    self.peek()
14991                                )));
14992                            }
14993                            self.advance();
14994                            let upper = Box::new(self.parse_partition_bound_expr()?);
14995                            PartitionOfBoundsAst::Range { lower, upper }
14996                        }
14997                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
14998                        Token::In => {
14999                            self.advance();
15000                            if !matches!(self.peek(), Token::LParen) {
15001                                return Err(self.err(format!(
15002                                    "expected '(' after FOR VALUES IN, got {:?}",
15003                                    self.peek()
15004                                )));
15005                            }
15006                            self.advance();
15007                            let mut values = Vec::new();
15008                            loop {
15009                                values.push(self.parse_expr(0)?);
15010                                match self.peek() {
15011                                    Token::Comma => {
15012                                        self.advance();
15013                                    }
15014                                    Token::RParen => {
15015                                        self.advance();
15016                                        break;
15017                                    }
15018                                    other => {
15019                                        return Err(self.err(format!(
15020                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
15021                                    )));
15022                                    }
15023                                }
15024                            }
15025                            if values.is_empty() {
15026                                return Err(self.err(
15027                                    "FOR VALUES IN requires at least one literal".to_string(),
15028                                ));
15029                            }
15030                            PartitionOfBoundsAst::List { values }
15031                        }
15032                        other => {
15033                            return Err(self.err(format!(
15034                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
15035                            )));
15036                        }
15037                    }
15038                }
15039            }
15040            other => {
15041                return Err(self.err(format!(
15042                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15043                )));
15044            }
15045        };
15046        Ok(PartitionOfSpec {
15047            parent_name,
15048            bounds,
15049        })
15050    }
15051
15052    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15053    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15054    /// markers (no-arg builtins) so the engine resolves them
15055    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15056    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15057        if !matches!(self.peek(), Token::LParen) {
15058            return Err(self.err(format!(
15059                "expected '(' before partition bound, got {:?}",
15060                self.peek()
15061            )));
15062        }
15063        self.advance();
15064        let expr = match self.peek() {
15065            Token::Ident(s) | Token::QuotedIdent(s)
15066                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15067            {
15068                let name = s.to_ascii_uppercase();
15069                self.advance();
15070                crate::ast::Expr::FunctionCall {
15071                    name,
15072                    args: Vec::new(),
15073                }
15074            }
15075            _ => self.parse_expr(0)?,
15076        };
15077        if !matches!(self.peek(), Token::RParen) {
15078            return Err(self.err(format!(
15079                "expected ')' after partition bound, got {:?}",
15080                self.peek()
15081            )));
15082        }
15083        self.advance();
15084        Ok(expr)
15085    }
15086
15087    /// v7.14.0 — true when the next tokens look like an inline
15088    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15089    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15090    /// — each followed by an optional name + `(...)`. Critical:
15091    /// a column NAMED `key` / `index` (PG accepts as ident) must
15092    /// NOT be mistaken for the KEY constraint shape. We disambig
15093    /// by requiring the keyword to be followed by either `(` or
15094    /// `<ident> (`.
15095    fn peek_mysql_inline_key_start(&self) -> bool {
15096        let cur = self.peek();
15097        // Shapes:
15098        //   KEY (cols)
15099        //   KEY name (cols)
15100        //   INDEX (cols)
15101        //   INDEX name (cols)
15102        //   UNIQUE KEY [name] (cols)
15103        //   UNIQUE INDEX [name] (cols)
15104        //   FULLTEXT [KEY|INDEX] [name] (cols)
15105        //   SPATIAL [KEY|INDEX] [name] (cols)
15106        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15107            // tokens at skip = the position AFTER the index-form
15108            // keywords (KEY/INDEX) have been consumed.
15109            match self.tokens.get(skip) {
15110                Some(Token::LParen) => true,
15111                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15112                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15113                }
15114                _ => false,
15115            }
15116        };
15117        // `INDEX` lexes as Token::Index (reserved), not as
15118        // Token::Ident("index"). Both shapes count as a KEY/INDEX
15119        // start; the peek helper below handles either.
15120        let is_key_or_index_tok = |t: &Token| -> bool {
15121            matches!(t, Token::Index)
15122                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15123        };
15124        match cur {
15125            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15126            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15127                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15128            }
15129            Token::Ident(s)
15130                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15131            {
15132                let nxt = self.tokens.get(self.pos + 1);
15133                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15134                    self.pos + 2
15135                } else {
15136                    self.pos + 1
15137                };
15138                after_keyword_followed_by_paren_or_ident_paren(after_after)
15139            }
15140            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15141                let nxt = self.tokens.get(self.pos + 1);
15142                if !nxt.is_some_and(is_key_or_index_tok) {
15143                    return false;
15144                }
15145                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15146            }
15147            _ => false,
15148        }
15149    }
15150
15151    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15152    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15153    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15154    /// returns Some(TableConstraint::Index) so the engine builds
15155    /// a real BTree index on the leading column (mysqldump
15156    /// `KEY idx_posts_author (author_id)` shape).
15157    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15158    /// (the storage layer has no matching AM).
15159    fn parse_mysql_inline_key(
15160        &mut self,
15161    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15162        // Detect UNIQUE prefix.
15163        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15164        {
15165            self.advance();
15166            true
15167        } else {
15168            false
15169        };
15170        // Consume FULLTEXT / SPATIAL prefix and record which one
15171        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15172        // dedicated TableConstraint variant so the engine can
15173        // build a tsvector-GIN; SPATIAL still has no matching
15174        // AM, so it falls back to accept-as-no-op.
15175        let mut is_fulltext = false;
15176        let mut is_spatial = false;
15177        if let Token::Ident(s) = self.peek().clone() {
15178            if s.eq_ignore_ascii_case("fulltext") {
15179                self.advance();
15180                is_fulltext = true;
15181            } else if s.eq_ignore_ascii_case("spatial") {
15182                self.advance();
15183                is_spatial = true;
15184            }
15185        }
15186        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15187        // (reserved); accept either token shape.
15188        match self.peek() {
15189            Token::Index => {
15190                self.advance();
15191            }
15192            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15193                self.advance();
15194            }
15195            other => {
15196                return Err(self.err(alloc::format!(
15197                    "expected KEY/INDEX in inline index declaration, got {other:?}"
15198                )));
15199            }
15200        }
15201        // Optional index name (an ident before the `(`).
15202        // v7.15.0 — capture the name when present so the engine
15203        // builds the secondary index under the user's chosen
15204        // name (matches mysqldump's `KEY idx_x (col)` shape).
15205        let mut idx_name: Option<String> = None;
15206        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15207            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15208        {
15209            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15210                idx_name = Some(s);
15211            }
15212        }
15213        // Optional `USING BTREE` / `USING HASH` (MySQL).
15214        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15215            self.advance();
15216            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15217                self.advance();
15218            }
15219        }
15220        // Required column list `(col [, col]*)`.
15221        if !matches!(self.peek(), Token::LParen) {
15222            return Err(self.err(alloc::format!(
15223                "expected '(' in inline KEY/INDEX, got {:?}",
15224                self.peek()
15225            )));
15226        }
15227        self.advance();
15228        let mut cols: Vec<String> = Vec::new();
15229        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15230            self.advance();
15231            cols.push(s);
15232            // Skip optional `(length)` per-column prefix.
15233            if matches!(self.peek(), Token::LParen) {
15234                let mut depth = 1usize;
15235                self.advance();
15236                while depth > 0 {
15237                    match self.peek() {
15238                        Token::LParen => depth += 1,
15239                        Token::RParen => depth -= 1,
15240                        Token::Eof => break,
15241                        _ => {}
15242                    }
15243                    self.advance();
15244                }
15245            }
15246            // Skip optional ASC / DESC.
15247            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15248                || matches!(self.peek(), Token::Asc | Token::Desc)
15249            {
15250                self.advance();
15251            }
15252            if matches!(self.peek(), Token::Comma) {
15253                self.advance();
15254                continue;
15255            }
15256            break;
15257        }
15258        if matches!(self.peek(), Token::RParen) {
15259            self.advance();
15260        }
15261        // Trailing options on the inline index — comment / etc.
15262        // Skip until comma or `)`.
15263        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15264            self.advance();
15265        }
15266        if cols.is_empty() {
15267            return Ok(None);
15268        }
15269        if is_unique {
15270            // Carry the captured idx_name on UNIQUE too so future
15271            // engine work can name the underlying BTree
15272            // accordingly; today the unique-constraint installer
15273            // synthesises the name itself, but Display round-trip
15274            // benefits from preserving it.
15275            Ok(Some(crate::ast::TableConstraint::Unique {
15276                name: idx_name,
15277                columns: cols,
15278                nulls_not_distinct: false,
15279                // MySQL inline UNIQUE KEY has no deferral vocabulary.
15280                deferrable: false,
15281                initially_deferred: false,
15282            }))
15283        } else if is_fulltext {
15284            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15285            // routes through `TableConstraint::FulltextIndex`;
15286            // the engine builds a tsvector-GIN over each named
15287            // column so MATCH AGAINST gets a real inverted
15288            // index instead of a silently-dropped declaration.
15289            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15290                name: idx_name,
15291                columns: cols,
15292            }))
15293        } else if is_spatial {
15294            // SPG has no native SPATIAL AM. Accept-as-no-op
15295            // (declaration is parsed, but no index is built).
15296            Ok(None)
15297        } else {
15298            // v7.15.0 — plain KEY / INDEX builds a real BTree
15299            // secondary index.
15300            Ok(Some(crate::ast::TableConstraint::Index {
15301                name: idx_name,
15302                columns: cols,
15303            }))
15304        }
15305    }
15306
15307    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15308    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15309    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15310    /// (in any order, separated by whitespace).
15311    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15312    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15313    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15314    /// bare ident here, and only the parenthesised form is reloptions (so this
15315    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15316    fn consume_with_reloptions(&mut self) {
15317        let is_with = matches!(
15318            self.peek(),
15319            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15320        );
15321        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15322            return;
15323        }
15324        self.advance(); // WITH
15325        self.advance(); // (
15326        let mut depth = 1u32;
15327        while depth > 0 && !matches!(self.peek(), Token::Eof) {
15328            match self.peek() {
15329                Token::LParen => depth += 1,
15330                Token::RParen => depth -= 1,
15331                _ => {}
15332            }
15333            self.advance();
15334        }
15335    }
15336
15337    /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15338    /// dropped with everything else here. The rest of the MySQL table
15339    /// options genuinely have no meaning for SPG's storage; the engine
15340    /// name does, because MySQL REFUSES one it does not know and a dump
15341    /// with a typo in it should not quietly become a table.
15342    fn consume_mysql_table_options(&mut self) -> Option<alloc::string::String> {
15343        let mut engine: Option<alloc::string::String> = None;
15344        loop {
15345            // Heuristic: a table option is an ident (or `DEFAULT`
15346            // reserved keyword) followed by `=` and an
15347            // ident / string / integer.
15348            let name_lc = match self.peek().clone() {
15349                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15350                Token::Default => alloc::string::String::from("default"),
15351                _ => break,
15352            };
15353            let known = matches!(
15354                name_lc.as_str(),
15355                "engine"
15356                    | "default"
15357                    | "charset"
15358                    | "collate"
15359                    | "auto_increment"
15360                    | "row_format"
15361                    | "comment"
15362                    | "pack_keys"
15363                    | "stats_persistent"
15364                    | "stats_auto_recalc"
15365                    | "stats_sample_pages"
15366                    | "key_block_size"
15367                    | "tablespace"
15368                    | "min_rows"
15369                    | "max_rows"
15370                    | "checksum"
15371                    | "delay_key_write"
15372                    | "insert_method"
15373                    | "data"
15374                    | "index"
15375                    | "encryption"
15376                    | "compression"
15377            );
15378            if !known {
15379                break;
15380            }
15381            self.advance(); // option name
15382            // `DEFAULT` optional prefix is followed by `CHARSET` /
15383            // `COLLATE`; consume the next ident too.
15384            if name_lc == "default" {
15385                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15386                    self.advance();
15387                }
15388            }
15389            if matches!(self.peek(), Token::Eq) {
15390                self.advance();
15391            }
15392            match self.peek().clone() {
15393                Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15394                    if name_lc == "engine" {
15395                        // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15396                        // engine it does not know and names it back
15397                        // exactly: `Unknown storage engine 'NoSuchEng'`,
15398                        // measured. The lexer folds a bare identifier, so
15399                        // the message quoted a name the dump did not
15400                        // contain, which is the one thing that message is
15401                        // for. Guarded the same way the column spelling
15402                        // is: the span runs to the next token, so what
15403                        // comes back has to be the same word.
15404                        let written = self
15405                            .source_span(self.pos, self.pos)
15406                            .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15407                            .filter(|raw| raw.eq_ignore_ascii_case(&v))
15408                            .map(alloc::string::String::from);
15409                        engine = Some(written.unwrap_or(v));
15410                    }
15411                    self.advance();
15412                }
15413                Token::Integer(_) => {
15414                    self.advance();
15415                }
15416                _ => {}
15417            }
15418        }
15419        engine
15420    }
15421
15422    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15423    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15424    /// sure (otherwise a column literally named `primary` would
15425    /// be mistaken).
15426    fn peek_table_level_pk_start(&self) -> bool {
15427        let cur = self.peek();
15428        let nxt = self.tokens.get(self.pos + 1);
15429        let nxt2 = self.tokens.get(self.pos + 2);
15430        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15431        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15432        let is_lparen = matches!(nxt2, Some(Token::LParen));
15433        is_primary && is_key && is_lparen
15434    }
15435
15436    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15437    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15438    /// (mailrs round-5 G10).
15439    fn peek_table_level_unique_start(&self) -> bool {
15440        let cur = self.peek();
15441        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15442        if !is_unique {
15443            return false;
15444        }
15445        let n1 = self.tokens.get(self.pos + 1);
15446        // Plain `UNIQUE (…)`.
15447        if matches!(n1, Some(Token::LParen)) {
15448            return true;
15449        }
15450        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15451        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15452        if !is_nulls {
15453            return false;
15454        }
15455        let n2 = self.tokens.get(self.pos + 2);
15456        let n3 = self.tokens.get(self.pos + 3);
15457        let n4 = self.tokens.get(self.pos + 4);
15458        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15459        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15460            return true;
15461        }
15462        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15463        if matches!(n2, Some(Token::Not))
15464            && matches!(n3, Some(Token::Distinct))
15465            && matches!(n4, Some(Token::LParen))
15466        {
15467            return true;
15468        }
15469        false
15470    }
15471
15472    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15473        self.advance(); // PRIMARY
15474        self.advance(); // KEY
15475        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15476        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15477        // 621 consumed and dropped them (the storing half of F08).
15478        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15479        Ok(crate::ast::TableConstraint::PrimaryKey {
15480            name: None,
15481            columns,
15482            deferrable,
15483            initially_deferred,
15484        })
15485    }
15486
15487    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15488        self.advance(); // UNIQUE
15489        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15490        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15491        // is `NULLS DISTINCT` per the SQL standard.
15492        let mut nulls_not_distinct = false;
15493        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15494            let n1 = self.tokens.get(self.pos + 1);
15495            let n2 = self.tokens.get(self.pos + 2);
15496            let is_not = matches!(n1, Some(Token::Not));
15497            let is_distinct = matches!(n2, Some(Token::Distinct));
15498            if is_not && is_distinct {
15499                self.advance(); // NULLS
15500                self.advance(); // NOT
15501                self.advance(); // DISTINCT
15502                nulls_not_distinct = true;
15503            } else if matches!(n1, Some(Token::Distinct)) {
15504                self.advance(); // NULLS
15505                self.advance(); // DISTINCT
15506            }
15507        }
15508        let columns = self.parse_paren_ident_list("UNIQUE")?;
15509        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15510        Ok(crate::ast::TableConstraint::Unique {
15511            name: None,
15512            columns,
15513            nulls_not_distinct,
15514            deferrable,
15515            initially_deferred,
15516        })
15517    }
15518
15519    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15520    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15521    /// expression.
15522    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15523    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15524    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15525    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15526    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15527    /// commit: `NOT` starts no other suffix here, but reading both
15528    /// tokens before advancing keeps the caller's error message intact
15529    /// if someone writes `NOT NULL` by mistake.
15530    fn parse_not_valid_suffix(&mut self) -> bool {
15531        if !matches!(self.peek(), Token::Not) {
15532            return false;
15533        }
15534        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15535        {
15536            return false;
15537        }
15538        self.advance();
15539        self.advance();
15540        true
15541    }
15542
15543    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15544        self.advance(); // EXCLUDE
15545        // Optional `USING <method>`.
15546        let mut method = None;
15547        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15548            self.advance();
15549            method = Some(match self.advance() {
15550                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15551                other => {
15552                    return Err(self.err(alloc::format!(
15553                        "expected index method after USING, got {other:?}"
15554                    )));
15555                }
15556            });
15557        }
15558        if !matches!(self.peek(), Token::LParen) {
15559            return Err(self.err(alloc::format!(
15560                "expected '(' after EXCLUDE, got {:?}",
15561                self.peek()
15562            )));
15563        }
15564        self.advance();
15565        let mut elements: Vec<(String, String)> = Vec::new();
15566        loop {
15567            let col = match self.advance() {
15568                Token::Ident(s) | Token::QuotedIdent(s) => s,
15569                other => {
15570                    return Err(self.err(alloc::format!(
15571                        "expected column name in EXCLUDE, got {other:?}"
15572                    )));
15573                }
15574            };
15575            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15576                return Err(self.err(alloc::format!(
15577                    "expected WITH after EXCLUDE column, got {:?}",
15578                    self.peek()
15579                )));
15580            }
15581            self.advance();
15582            let op = match self.advance() {
15583                Token::InetOverlap => String::from("&&"),
15584                Token::Intersects => String::from("?#"),
15585                Token::IsBelow => String::from("<^"),
15586                Token::IsAbove => String::from(">^"),
15587                Token::PatternLt => String::from("~<~"),
15588                Token::PatternLtEq => String::from("~<=~"),
15589                Token::PatternGt => String::from("~>~"),
15590                Token::PatternGtEq => String::from("~>=~"),
15591                Token::TsMatchOld => String::from("@@@"),
15592                Token::Eq => String::from("="),
15593                Token::JsonContains => String::from("@>"),
15594                Token::JsonContainedBy => String::from("<@"),
15595                Token::OverLeft => String::from("&<"),
15596                Token::OverRight => String::from("&>"),
15597                other => {
15598                    return Err(self.err(alloc::format!(
15599                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15600                    )));
15601                }
15602            };
15603            elements.push((col, op));
15604            if matches!(self.peek(), Token::Comma) {
15605                self.advance();
15606                continue;
15607            }
15608            break;
15609        }
15610        if !matches!(self.peek(), Token::RParen) {
15611            return Err(self.err(alloc::format!(
15612                "expected ')' to close EXCLUDE, got {:?}",
15613                self.peek()
15614            )));
15615        }
15616        self.advance();
15617        Ok(crate::ast::TableConstraint::Exclude {
15618            name: None,
15619            method,
15620            elements,
15621        })
15622    }
15623
15624    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15625        self.advance(); // CHECK
15626        if !matches!(self.peek(), Token::LParen) {
15627            return Err(self.err(alloc::format!(
15628                "expected '(' after CHECK, got {:?}",
15629                self.peek()
15630            )));
15631        }
15632        self.advance();
15633        let expr = self.parse_expr(0)?;
15634        if !matches!(self.peek(), Token::RParen) {
15635            return Err(self.err(alloc::format!(
15636                "expected ')' to close CHECK predicate, got {:?}",
15637                self.peek()
15638            )));
15639        }
15640        self.advance();
15641        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15642        // are no existing rows for PG to skip, so it rejects the suffix.
15643        Ok(crate::ast::TableConstraint::Check {
15644            name: None,
15645            expr,
15646            not_valid: false,
15647        })
15648    }
15649
15650    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15651    fn peek_table_level_check_start(&self) -> bool {
15652        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15653    }
15654
15655    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15656    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15657    /// on the dedicated FK path (`parse_table_level_fk` consumes its
15658    /// own CONSTRAINT prefix).
15659    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
15660        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15661            return None;
15662        }
15663        // tokens[pos+1] is the constraint name (any ident-like);
15664        // tokens[pos+2] is the kind keyword.
15665        match self.tokens.get(self.pos + 2) {
15666            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
15667                Some(NamedTableConstraintKind::Check)
15668            }
15669            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
15670                Some(NamedTableConstraintKind::Unique)
15671            }
15672            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
15673                Some(NamedTableConstraintKind::PrimaryKey)
15674            }
15675            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
15676                Some(NamedTableConstraintKind::Exclude)
15677            }
15678            _ => None,
15679        }
15680    }
15681
15682    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
15683        if !matches!(self.peek(), Token::LParen) {
15684            return Err(self.err(alloc::format!(
15685                "expected '(' after {ctx}, got {:?}",
15686                self.peek()
15687            )));
15688        }
15689        self.advance();
15690        let mut out = Vec::new();
15691        loop {
15692            out.push(self.expect_ident_like()?);
15693            match self.peek() {
15694                Token::Comma => {
15695                    self.advance();
15696                }
15697                Token::RParen => {
15698                    self.advance();
15699                    break;
15700                }
15701                other => {
15702                    return Err(self.err(alloc::format!(
15703                        "expected ',' or ')' in {ctx} list, got {other:?}"
15704                    )));
15705                }
15706            }
15707        }
15708        if out.is_empty() {
15709            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
15710        }
15711        Ok(out)
15712    }
15713
15714    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
15715    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
15716    /// table-level FK; a column def never starts with either keyword
15717    /// (column names are not in this reserved set).
15718    fn peek_constraint_or_fk_start(&self) -> bool {
15719        let is_constraint_kw = matches!(
15720            self.peek(),
15721            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
15722        );
15723        let is_foreign_kw = matches!(
15724            self.peek(),
15725            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
15726        );
15727        is_constraint_kw || is_foreign_kw
15728    }
15729
15730    /// v7.6.0 — parse a table-level FK clause:
15731    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
15732    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
15733    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
15734        let mut name: Option<String> = None;
15735        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
15736            self.advance();
15737            name = Some(self.expect_ident_like()?);
15738        }
15739        // `FOREIGN`
15740        match self.advance() {
15741            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
15742            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
15743        }
15744        // `KEY`
15745        match self.advance() {
15746            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
15747            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
15748        }
15749        // `(col, col, ...)`
15750        if !matches!(self.peek(), Token::LParen) {
15751            return Err(self.err(format!(
15752                "expected '(' after FOREIGN KEY, got {:?}",
15753                self.peek()
15754            )));
15755        }
15756        self.advance();
15757        let mut columns = Vec::new();
15758        loop {
15759            columns.push(self.expect_ident_like()?);
15760            match self.peek() {
15761                Token::Comma => {
15762                    self.advance();
15763                }
15764                Token::RParen => {
15765                    self.advance();
15766                    break;
15767                }
15768                other => {
15769                    return Err(self.err(format!(
15770                        "expected ',' or ')' in FK column list, got {other:?}"
15771                    )));
15772                }
15773            }
15774        }
15775        if columns.is_empty() {
15776            return Err(self.err("FOREIGN KEY requires at least one column".into()));
15777        }
15778        let (
15779            parent_table,
15780            parent_columns,
15781            on_delete,
15782            on_update,
15783            match_type,
15784            deferrable,
15785            initially_deferred,
15786        ) = self.parse_references_tail(columns.len())?;
15787        Ok(ForeignKeyConstraint {
15788            name,
15789            columns,
15790            parent_table,
15791            parent_columns,
15792            on_delete,
15793            on_update,
15794            match_type,
15795            deferrable,
15796            initially_deferred,
15797        })
15798    }
15799
15800    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
15801    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
15802    /// the local column count, used to default the parent column
15803    /// list when omitted (SQL spec: parent's PK is implied).
15804    fn parse_references_tail(
15805        &mut self,
15806        expected_arity: usize,
15807    ) -> Result<
15808        (
15809            String,
15810            Vec<String>,
15811            FkAction,
15812            FkAction,
15813            crate::ast::MatchType,
15814            // v7.39 (round 288) — deferrable, initially_deferred.
15815            bool,
15816            bool,
15817        ),
15818        ParseError,
15819    > {
15820        match self.advance() {
15821            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
15822            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
15823        }
15824        let parent_table = self.expect_ident_like()?;
15825        let mut parent_columns: Vec<String> = Vec::new();
15826        if matches!(self.peek(), Token::LParen) {
15827            self.advance();
15828            loop {
15829                parent_columns.push(self.expect_ident_like()?);
15830                match self.peek() {
15831                    Token::Comma => {
15832                        self.advance();
15833                    }
15834                    Token::RParen => {
15835                        self.advance();
15836                        break;
15837                    }
15838                    other => {
15839                        return Err(self.err(format!(
15840                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
15841                        )));
15842                    }
15843                }
15844            }
15845        }
15846        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
15847            return Err(self.err(format!(
15848                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
15849                expected_arity,
15850                parent_columns.len()
15851            )));
15852        }
15853        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
15854        // it between the referenced column list and the ON / DEFERRABLE
15855        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
15856        // is skipped when any referencing column is NULL), so SIMPLE —
15857        // the default, and the only spelling pg_dump emits — is accepted
15858        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
15859        // mixed-NULL rule, which is not wired yet; reject them honestly
15860        // rather than silently applying SIMPLE (PG itself errors on
15861        // MATCH PARTIAL as "not yet implemented").
15862        let mut match_type = crate::ast::MatchType::Simple;
15863        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
15864            self.advance();
15865            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
15866            // SIMPLE / PARTIAL arrive as bare identifiers.
15867            let kind = match self.advance() {
15868                Token::Full => "FULL".to_string(),
15869                Token::Ident(s) => s.to_uppercase(),
15870                other => {
15871                    return Err(self.err(format!(
15872                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
15873                    )));
15874                }
15875            };
15876            match kind.as_str() {
15877                "SIMPLE" => {} // Default — match_type stays Simple.
15878                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
15879                // when ALL referencing columns are NULL; a mixed-NULL key errors.
15880                "FULL" => match_type = crate::ast::MatchType::Full,
15881                "PARTIAL" => {
15882                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
15883                }
15884                _ => {
15885                    return Err(self.err(format!(
15886                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
15887                    )));
15888                }
15889            }
15890        }
15891        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
15892        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
15893        // <action>` / `ON UPDATE <action>` in either order. PG /
15894        // pg_dump emits the timing clause AFTER the ON clauses
15895        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
15896        // but the SQL spec allows either order. We loop over
15897        // every possible trailer and dispatch on the next token,
15898        // stopping when nothing matches. Phase 3.1 changes the
15899        // bare DEFERRABLE form from hard-error to accept-as-
15900        // immediate; SPG is single-writer with no deferred-
15901        // constraint window so the runtime semantics are always
15902        // immediate even when INITIALLY DEFERRED is requested.
15903        // PG's default referential action (no ON DELETE / ON UPDATE
15904        // clause) is NO ACTION, not RESTRICT — the two enforce
15905        // identically in SPG (single-writer, no deferred window; see the
15906        // shared match arm in constraints.rs) but information_schema.
15907        // referential_constraints must report NO ACTION to match PG.
15908        let mut on_delete = FkAction::NoAction;
15909        let mut on_update = FkAction::NoAction;
15910        let mut seen_on_delete = false;
15911        let mut seen_on_update = false;
15912        let mut deferrable = false;
15913        let mut initially_deferred = false;
15914        loop {
15915            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
15916            let before = self.pos;
15917            let (d, idef) = self.consume_deferrable_clauses_timed()?;
15918            if self.pos != before {
15919                deferrable = d;
15920                initially_deferred = idef;
15921                continue;
15922            }
15923            // ON DELETE / ON UPDATE.
15924            if !matches!(self.peek(), Token::On) {
15925                break;
15926            }
15927            self.advance();
15928            let which = self.advance();
15929            let action = self.parse_fk_action()?;
15930            match which {
15931                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
15932                    if seen_on_delete {
15933                        return Err(self.err("ON DELETE specified twice".into()));
15934                    }
15935                    seen_on_delete = true;
15936                    on_delete = action;
15937                }
15938                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
15939                    if seen_on_update {
15940                        return Err(self.err("ON UPDATE specified twice".into()));
15941                    }
15942                    seen_on_update = true;
15943                    on_update = action;
15944                }
15945                other => {
15946                    return Err(
15947                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
15948                    );
15949                }
15950            }
15951        }
15952        Ok((
15953            parent_table,
15954            parent_columns,
15955            on_delete,
15956            on_update,
15957            match_type,
15958            deferrable,
15959            initially_deferred,
15960        ))
15961    }
15962
15963    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
15964    /// NO ACTION`.
15965    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
15966        match self.advance() {
15967            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
15968            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
15969            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
15970                Token::Null => Ok(FkAction::SetNull),
15971                Token::Default => Ok(FkAction::SetDefault),
15972                other => Err(self.err(format!(
15973                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
15974                ))),
15975            },
15976            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
15977                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
15978                other => Err(self.err(format!(
15979                    "expected ACTION after NO in FK action, got {other:?}"
15980                ))),
15981            },
15982            other => Err(self.err(format!(
15983                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
15984            ))),
15985        }
15986    }
15987
15988    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
15989    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
15990    fn consume_if_not_exists(&mut self) -> bool {
15991        // `IF` arrives as a bare Ident (we don't reserve it because it
15992        // also appears mid-expression in PG, though we don't support
15993        // those forms yet).
15994        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
15995        if !looks_like_if {
15996            return false;
15997        }
15998        // Peek one ahead before committing: only consume IF when it's
15999        // actually `IF NOT EXISTS`.
16000        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
16001            return false;
16002        }
16003        if !matches!(
16004            self.tokens.get(self.pos + 2),
16005            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16006        ) {
16007            return false;
16008        }
16009        self.advance(); // IF
16010        self.advance(); // NOT
16011        self.advance(); // EXISTS
16012        true
16013    }
16014
16015    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
16016    /// Consumes IF EXISTS as a pair; returns false otherwise
16017    /// without consuming any tokens.
16018    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
16019    /// ENABLE/DISABLE/FORCE/NO FORCE.
16020    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
16021        for kw in ["row", "level", "security"] {
16022            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
16023            {
16024                return Err(self.err(alloc::format!(
16025                    "expected {} in ROW LEVEL SECURITY, got {:?}",
16026                    kw.to_ascii_uppercase(),
16027                    self.peek()
16028                )));
16029            }
16030            self.advance();
16031        }
16032        Ok(())
16033    }
16034
16035    fn consume_if_exists(&mut self) -> bool {
16036        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16037        if !looks_like_if {
16038            return false;
16039        }
16040        if !matches!(
16041            self.tokens.get(self.pos + 1),
16042            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16043        ) {
16044            return false;
16045        }
16046        self.advance(); // IF
16047        self.advance(); // EXISTS
16048        true
16049    }
16050
16051    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16052    /// qualifiers after an index column ref. ASC / DESC are
16053    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16054    /// We accept and discard them since single-column BTree
16055    /// stores rows in natural key order today.
16056    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16057    /// ORDER BY key. Returns None when absent.
16058    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16059        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16060            return Ok(None);
16061        }
16062        self.advance();
16063        match self.advance() {
16064            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16065            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16066            other => Err(self.err(alloc::format!(
16067                "expected FIRST or LAST after NULLS, got {other:?}"
16068            ))),
16069        }
16070    }
16071
16072    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16073    /// rather than discarded.
16074    ///
16075    /// SPG's index does not scan in a direction — column ordering is
16076    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16077    /// reproduction of the DDL, and dropping the clause meant
16078    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16079    /// dump lost it, and a schema diff saw drift on every run.
16080    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16081        let mut order = crate::ast::IndexColumnOrder::default();
16082        loop {
16083            match self.peek() {
16084                Token::Asc => {
16085                    self.advance();
16086                }
16087                Token::Desc => {
16088                    order.descending = true;
16089                    self.advance();
16090                }
16091                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16092                    let look = self.tokens.get(self.pos + 1);
16093                    if matches!(
16094                        look,
16095                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16096                            || k.eq_ignore_ascii_case("last")
16097                    ) {
16098                        self.advance();
16099                        order.nulls_first = Some(matches!(
16100                            self.advance(),
16101                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
16102                        ));
16103                    } else {
16104                        break;
16105                    }
16106                }
16107                _ => break,
16108            }
16109        }
16110        order
16111    }
16112
16113    fn parse_create_index_stmt_after_create(
16114        &mut self,
16115        is_unique: bool,
16116    ) -> Result<Statement, ParseError> {
16117        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16118        debug_assert!(matches!(self.peek(), Token::Index));
16119        self.advance();
16120        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16121        // SPG's CREATE INDEX is synchronous end-to-end today (real
16122        // CONCURRENTLY variant with restartable scans queues with
16123        // v7.39 indexes epic), so the modifier has no runtime effect
16124        // — same accept-and-no-op shape as v7.37.16.5 DETACH
16125        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16126        // VIEW CONCURRENTLY.
16127        let mut concurrently = false;
16128        if matches!(
16129            self.peek(),
16130            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16131        ) {
16132            self.advance();
16133            concurrently = true;
16134        }
16135        let if_not_exists = self.consume_if_not_exists();
16136        // v7.39 (read01 round 93) — the index name is optional (PG since
16137        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16138        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16139        // was given; leave it empty and the engine derives a PG-style
16140        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16141        let name = if matches!(self.peek(), Token::On) {
16142            String::new()
16143        } else {
16144            self.expect_ident_like()?
16145        };
16146        if !matches!(self.peek(), Token::On) {
16147            return Err(self.err(format!(
16148                "expected ON after CREATE INDEX <name>, got {:?}",
16149                self.peek()
16150            )));
16151        }
16152        self.advance();
16153        let table = self.expect_ident_like()?;
16154        // Optional `USING <method>` — only recognised method in v2.0 is
16155        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16156        // ident `using` (we don't promote it to a reserved keyword
16157        // because it isn't reserved anywhere else in our SQL surface).
16158        let mut method_name: Option<String> = None;
16159        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16160            self.advance();
16161            let m = self.expect_ident_like()?;
16162            method_name = Some(m.to_ascii_lowercase());
16163            match m.to_ascii_lowercase().as_str() {
16164                "hnsw" => IndexMethod::Hnsw,
16165                "btree" => IndexMethod::BTree,
16166                "brin" => IndexMethod::Brin,
16167                // v7.12.3 — real GIN inverted index over `tsvector`.
16168                // v7.9.26b's `USING gin` → BTree silent fallback is
16169                // gone; the engine validates that the indexed column
16170                // is `tsvector` at CREATE INDEX time.
16171                "gin" => IndexMethod::Gin,
16172                // v7.9.26b — PG `pg_dump` emits `USING gist` /
16173                // `USING spgist` / `USING hash` for their built-in
16174                // AMs that SPG doesn't have a matching
16175                // implementation for; degrade to BTree on the
16176                // leading column so the schema loads + the index
16177                // catalogue stays consistent. Operator pays the
16178                // planner cost only for the queries that would have
16179                // used the specialised AM.
16180                "gist" | "spgist" | "hash" => IndexMethod::BTree,
16181                // v7.11.3 — pgvector ships both `ivfflat` and
16182                // `hnsw`. Customers shouldn't have to choose
16183                // their on-disk index method based on what SPG
16184                // implements; accept `ivfflat` as a synonym for
16185                // `hnsw` so PG schemas using either method drop
16186                // in. The vector distance op (`<->` / `<#>` /
16187                // `<=>`) at query time still picks the metric.
16188                "ivfflat" => IndexMethod::Hnsw,
16189                other => {
16190                    return Err(self.err(alloc::format!(
16191                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16192                    )));
16193                }
16194            }
16195        } else {
16196            IndexMethod::BTree
16197        };
16198        if !matches!(self.peek(), Token::LParen) {
16199            return Err(self.err(format!(
16200                "expected '(' before indexed column, got {:?}",
16201                self.peek()
16202            )));
16203        }
16204        self.advance();
16205        // v6.8.2 — accept either a bare column ident (legacy) or
16206        // an expression `fn(col, …)` for expression indexes.
16207        // Distinguish by peeking the token *after* the current
16208        // ident: `ident )` is the legacy column-only path;
16209        // anything else triggers the Pratt expression parser.
16210        // (`advance()` uses `mem::replace` to nil out the current
16211        // slot, so we can't save+rewind cleanly — peek-ahead via
16212        // direct index avoids the mutation.)
16213        let mut opclass: Option<String> = None;
16214        let mut key_collation: Option<String> = None;
16215        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16216            // Single column with `)` immediately after — fast path.
16217            // v7.9.29 — also: bare column followed by `,` (the
16218            // multi-column form `(a, b, c)`). Without this branch
16219            // the leading ident gets pulled into `parse_expr`
16220            // which then sets `expression = Some(Column(a))` and
16221            // breaks Display round-trip on the multi-column shape.
16222            Token::Ident(s) | Token::QuotedIdent(s)
16223                if matches!(
16224                    self.tokens.get(self.pos + 1),
16225                    Some(Token::RParen | Token::Comma)
16226                ) =>
16227            {
16228                self.advance();
16229                (s, None)
16230            }
16231            // v7.9.22 — single column followed by a pgvector
16232            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16233            // v7.15.0 — capture the opclass instead of discarding
16234            // it so the engine can dispatch (e.g. `gin_trgm_ops`
16235            // → real trigram-shingle GIN over a TEXT column).
16236            // Vector/HNSW opclasses still take their distance
16237            // metric from the query operator (`<->` / `<#>` /
16238            // `<=>`), so for those callers the opclass stays
16239            // informational.
16240            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16241            // opclass: `(embedding public.vector_cosine_ops)`. Strip
16242            // the schema and dispatch on the bare opclass, the same
16243            // treatment table/type names get.
16244            Token::Ident(s) | Token::QuotedIdent(s)
16245                if matches!(
16246                    self.tokens.get(self.pos + 1),
16247                    Some(Token::Ident(_) | Token::QuotedIdent(_))
16248                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16249                    && matches!(
16250                        self.tokens.get(self.pos + 3),
16251                        Some(Token::Ident(op) | Token::QuotedIdent(op))
16252                            if is_vector_opclass_name(op)
16253                    ) =>
16254            {
16255                self.advance(); // column name
16256                self.advance(); // schema qualifier
16257                self.advance(); // dot
16258                let op_tok = self.advance();
16259                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16260                    opclass = Some(op.to_ascii_lowercase());
16261                }
16262                (s, None)
16263            }
16264            // r1038 — an operator class is recognised by its POSITION, not
16265            // by a list of names. It used to be `is_vector_opclass_name`,
16266            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16267            // sentori's migration wrote — was a syntax error while
16268            // `USING gin (doc)` parsed. Anything sitting between a column
16269            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16270            // two bare identifiers in a row are not valid there otherwise.
16271            Token::Ident(s) | Token::QuotedIdent(s)
16272                if matches!(
16273                    self.tokens.get(self.pos + 1),
16274                    Some(Token::Ident(op) | Token::QuotedIdent(op))
16275                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
16276                            self.tokens.get(self.pos + 2)
16277                        )
16278                ) =>
16279            {
16280                self.advance(); // column name
16281                // Capture the opclass token, lower-cased for
16282                // case-insensitive engine dispatch.
16283                let op_tok = self.advance();
16284                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16285                    opclass = Some(op.to_ascii_lowercase());
16286                }
16287                (s, None)
16288            }
16289            Token::Ident(_) | Token::QuotedIdent(_) => {
16290                // v7.39 (round 538) — an explicit COLLATE on the key,
16291                // read by LOOKAHEAD because `parse_expr` absorbs the
16292                // clause as a no-op (SPG orders text by bytes, which is
16293                // the C collation, so it changes nothing to honour). PG
16294                // still PRINTS it: an explicitly written `"C"` and the
16295                // collation a column inherits are different collation
16296                // OBJECTS even where they sort identically, which is why
16297                // `(a COLLATE "C")` shows on a C-collation database too.
16298                if matches!(
16299                    self.tokens.get(self.pos + 1),
16300                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16301                ) {
16302                    key_collation = match self.tokens.get(self.pos + 2) {
16303                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16304                            Some(n.clone())
16305                        }
16306                        _ => None,
16307                    };
16308                }
16309                // v7.39.2 — the clause is read by the LOOKAHEAD above and
16310                // belongs to the KEY, not to the expression. Since
16311                // `COLLATE` became a node, letting `parse_expr` build one
16312                // here put the collation in twice and the key deparsed as
16313                // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16314                // is the same idea and already exists, so this borrows it:
16315                // absorb into the side channel, and the key's own
16316                // lookahead is what carries it.
16317                // v7.39.2 — and the key can only CARRY the byte-order
16318                // spellings. Absorbing into the side channel accepts any
16319                // name, so suppressing the node here without this check
16320                // silently accepted `(name COLLATE "en_US")`, which SPG's
16321                // index cannot honour — a refusal that was doing real
16322                // work, removed by the suppression and put back here.
16323                if let Some(name) = &key_collation {
16324                    let lc = name.to_ascii_lowercase();
16325                    let byte_order = matches!(
16326                        lc.as_str(),
16327                        "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16328                    );
16329                    let mysql_ok = self.mysql_dialect
16330                        && (lc.ends_with("_ci")
16331                            || lc.ends_with("_bin")
16332                            || lc == "binary"
16333                            || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16334                    if !byte_order && !mysql_ok {
16335                        return Err(self.err(alloc::format!(
16336                            "COLLATE {name:?} is not supported in this position: an index \
16337                             key carries the byte-order spellings only. Declare it on the \
16338                             column (`x text COLLATE {name:?}`) instead"
16339                        )));
16340                    }
16341                }
16342                let saved_key_ctx = self.in_order_by_key;
16343                self.in_order_by_key = true;
16344                let key_expr = self.parse_expr(0);
16345                self.in_order_by_key = saved_key_ctx;
16346                let key_expr = key_expr?;
16347                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16348                    self.err("expression index key must reference at least one column".into())
16349                })?;
16350                (primary, Some(key_expr))
16351            }
16352            // v7.37.43-T4 — parenthesised expression index key
16353            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16354            // PG's CREATE INDEX requires the expression to be in
16355            // its own parens to disambiguate function calls from
16356            // column lists, so this `LParen` is the inner open-paren
16357            // of an expression key. parse_expr handles the recursive
16358            // descent and consumes the matching `RParen`.
16359            Token::LParen => {
16360                let key_expr = self.parse_expr(0)?;
16361                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16362                    self.err("expression index key must reference at least one column".into())
16363                })?;
16364                (primary, Some(key_expr))
16365            }
16366            other => {
16367                return Err(self.err(format!(
16368                    "expected column ident or expression, got {other:?}"
16369                )));
16370            }
16371        };
16372        // v7.9.14 — accept extra comma-separated columns inside
16373        // the index key parens (`CREATE INDEX … (a, b, c)`).
16374        // mailrs F2. Each extra column may carry an optional
16375        // `ASC` / `DESC` / `NULLS FIRST` / `NULLS LAST` clause
16376        // — parsed and discarded; SPG doesn't honour direction
16377        // on a BTree index today (column ordering is intrinsic
16378        // to the storage). v7.10 will widen to genuine composite
16379        // index keys.
16380        let mut extra_columns: Vec<String> = Vec::new();
16381        // The leading column may also have ASC/DESC after it — and that
16382        // one is the column SPG indexes, so its clause is kept.
16383        let key_order = self.consume_optional_index_column_qualifiers();
16384        while matches!(self.peek(), Token::Comma) {
16385            self.advance();
16386            let extra = self.expect_ident_like()?;
16387            let _ = self.consume_optional_index_column_qualifiers();
16388            extra_columns.push(extra);
16389        }
16390        if !matches!(self.peek(), Token::RParen) {
16391            return Err(self.err(format!(
16392                "expected ')' after indexed column / expression, got {:?}",
16393                self.peek()
16394            )));
16395        }
16396        self.advance();
16397        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16398        // index-only-scan annotation. Bare ident (not a reserved
16399        // keyword) so we test by case-insensitive string match.
16400        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16401        {
16402            self.advance();
16403            if !matches!(self.peek(), Token::LParen) {
16404                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16405            }
16406            self.advance();
16407            let mut cols = Vec::new();
16408            loop {
16409                cols.push(self.expect_ident_like()?);
16410                match self.peek() {
16411                    Token::Comma => {
16412                        self.advance();
16413                    }
16414                    Token::RParen => {
16415                        self.advance();
16416                        break;
16417                    }
16418                    other => {
16419                        return Err(self.err(format!(
16420                            "expected ',' or ')' in INCLUDE list, got {other:?}"
16421                        )));
16422                    }
16423                }
16424            }
16425            cols
16426        } else {
16427            Vec::new()
16428        };
16429        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16430        // storage parameters. pgvector emits `WITH (lists = N)` for
16431        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16432        // SPG's HNSW picks its own parameters today (tunable via
16433        // env vars), so the WITH clause is informational and dropped.
16434        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16435            self.advance();
16436            if !matches!(self.peek(), Token::LParen) {
16437                return Err(self.err(format!(
16438                    "expected '(' after WITH in CREATE INDEX, got {:?}",
16439                    self.peek()
16440                )));
16441            }
16442            self.advance();
16443            loop {
16444                if matches!(self.peek(), Token::RParen) {
16445                    self.advance();
16446                    break;
16447                }
16448                // Drain `key = value` or bare `key` tokens.
16449                let _ = self.advance(); // key
16450                if matches!(self.peek(), Token::Eq) {
16451                    self.advance();
16452                    let _ = self.advance(); // value (int / string / ident)
16453                }
16454                match self.peek() {
16455                    Token::Comma => {
16456                        self.advance();
16457                    }
16458                    Token::RParen => {
16459                        self.advance();
16460                        break;
16461                    }
16462                    other => {
16463                        return Err(self.err(format!(
16464                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
16465                        )));
16466                    }
16467                }
16468            }
16469        }
16470        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16471        // which sits between the key list and the WHERE clause.
16472        let mut nulls_not_distinct = false;
16473        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16474            let n1 = self.tokens.get(self.pos + 1);
16475            let n2 = self.tokens.get(self.pos + 2);
16476            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16477                self.advance(); // NULLS
16478                self.advance(); // NOT
16479                self.advance(); // DISTINCT
16480                nulls_not_distinct = true;
16481            } else if matches!(n1, Some(Token::Distinct)) {
16482                self.advance(); // NULLS
16483                self.advance(); // DISTINCT
16484            }
16485        }
16486        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16487        let partial_predicate = if matches!(self.peek(), Token::Where) {
16488            self.advance();
16489            Some(self.parse_expr(0)?)
16490        } else {
16491            None
16492        };
16493        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16494        // sense: uniqueness over an ANN structure has no clean
16495        // semantics. Reject early. (BRIN UNIQUE is similarly
16496        // meaningless — block both.)
16497        if is_unique && !matches!(method, IndexMethod::BTree) {
16498            return Err(self.err(alloc::format!(
16499                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16500                method
16501            )));
16502        }
16503        Ok(Statement::CreateIndex(CreateIndexStatement {
16504            concurrently,
16505            name,
16506            key_order,
16507            key_collation,
16508            table,
16509            column,
16510            nulls_not_distinct,
16511            method,
16512            if_not_exists,
16513            included_columns,
16514            partial_predicate,
16515            extra_columns: extra_columns.clone(),
16516            expression,
16517            is_unique,
16518            opclass,
16519            method_name,
16520        }))
16521    }
16522
16523    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16524    /// column-level `REFERENCES ...` clause. The trailing FK is
16525    /// normalised into table-level shape (single-element columns +
16526    /// parent_columns) so the engine sees one uniform constraint list.
16527    fn parse_column_def_with_fk(
16528        &mut self,
16529    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16530        let col = self.parse_column_def()?;
16531        // v7.39 (round 308, V29) — an explicitly named inline FK:
16532        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16533        // loop leaves this spelling intact precisely so the name can be
16534        // kept here; PG reports it in violation messages and matches it
16535        // in `SET CONSTRAINTS`.
16536        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16537        {
16538            self.advance();
16539            Some(self.expect_ident_like()?)
16540        } else {
16541            None
16542        };
16543        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16544        let inline_references = matches!(
16545            self.peek(),
16546            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16547        );
16548        if !inline_references {
16549            return Ok((col, None));
16550        }
16551        let (
16552            parent_table,
16553            parent_columns,
16554            on_delete,
16555            on_update,
16556            match_type,
16557            deferrable,
16558            initially_deferred,
16559        ) = self.parse_references_tail(1)?;
16560        let fk = ForeignKeyConstraint {
16561            name: declared_name,
16562            columns: vec![col.name.clone()],
16563            parent_table,
16564            parent_columns,
16565            on_delete,
16566            on_update,
16567            match_type,
16568            deferrable,
16569            initially_deferred,
16570        };
16571        Ok((col, Some(fk)))
16572    }
16573
16574    /// v7.13.0 — parse a column type (consuming the type ident and
16575    /// any trailing parameters / `[]`), without surrounding column
16576    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16577    /// Returns the resolved `ColumnTypeName` plus implied
16578    /// `(auto_increment, not_null)` flags from PG SERIAL family
16579    /// shorthands — callers that don't expect those (ALTER COLUMN
16580    /// TYPE) can discard them.
16581    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16582        let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16583        Ok(ty)
16584    }
16585
16586    #[allow(clippy::type_complexity)]
16587    fn parse_type_with_implied_flags(
16588        &mut self,
16589    ) -> Result<
16590        (
16591            ColumnTypeName,
16592            bool,
16593            bool,
16594            Option<String>,
16595            Collation,
16596            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16597            bool,
16598            // v7.39 (round 676) — the collation NAME as written, which the
16599            // `Collation` enum above cannot carry.
16600            Option<String>,
16601            bool,
16602            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16603            // list captured at type-parse time. None for all
16604            // non-ENUM types.
16605            Option<Vec<String>>,
16606            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16607            // list. Distinct from ENUM (subset semantics).
16608            Option<Vec<String>>,
16609            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16610            // width, lost when the type collapses to SmallInt / Int.
16611            Option<MysqlIntWidth>,
16612            // v7.39 (round 424) — declared fractional-seconds precision of a
16613            // MySQL temporal column (bare spelling = 0). None under PG.
16614            Option<u8>,
16615            // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
16616            // two are different types on MySQL and SPG stores both as
16617            // `Timestamp`, so the spelling has to travel separately or
16618            // a dump silently rewrites the column.
16619            bool,
16620            // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
16621            // display hint: it rounds on write.
16622            Option<(u8, u8)>,
16623        ),
16624        ParseError,
16625    > {
16626        let mut ty_ident = match self.advance() {
16627            Token::Ident(s) => s,
16628            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16629            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16630            // '<span>'` literal grammar. As a column type it lands
16631            // here directly; downstream resolution still uses the
16632            // canonical lowercase string.
16633            Token::Interval => "interval".to_string(),
16634            other => {
16635                return Err(ParseError {
16636                    message: format!("expected column type, got {other:?}"),
16637                    token_pos: self.consumed_pos(),
16638                });
16639            }
16640        };
16641        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16642        // pg_dump qualifies extension types (`public.vector(1024)`).
16643        // SPG is single-namespace; drop the schema and resolve the
16644        // bare type — same treatment table names already get.
16645        while matches!(self.peek(), Token::Dot) {
16646            self.advance();
16647            ty_ident = self.expect_ident_like()?;
16648        }
16649        let mut implied_auto_increment = false;
16650        let mut implied_not_null = false;
16651        let mut user_type_ref: Option<String> = None;
16652        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
16653        // value list, captured here and bubbled up through the
16654        // ColumnDef so the engine can attach it to the column
16655        // schema (and validate INSERT cells against it).
16656        let mut inline_enum_variants: Option<Vec<String>> = None;
16657        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
16658        let mut inline_set_variants: Option<Vec<String>> = None;
16659        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
16660        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
16661        // collapses to SmallInt / Int. Only under the MySQL dialect.
16662        let mut mysql_int_width: Option<MysqlIntWidth> = None;
16663        // v7.39 (round 424) — the declared fractional-seconds precision of a
16664        // MySQL temporal column. Set by the temporal arms below; stays None
16665        // for PG (whose temporal columns keep full microseconds).
16666        let mut mysql_fsp: Option<u8> = None;
16667        let mut mysql_declared_timestamp = false;
16668        let mut mysql_float_md: Option<(u8, u8)> = None;
16669        let mut ty = match ty_ident.as_str() {
16670            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
16671            "smallserial" | "serial2" => {
16672                implied_auto_increment = true;
16673                implied_not_null = true;
16674                ColumnTypeName::SmallInt
16675            }
16676            "serial" | "serial4" => {
16677                implied_auto_increment = true;
16678                implied_not_null = true;
16679                ColumnTypeName::Int
16680            }
16681            "bigserial" | "serial8" => {
16682                implied_auto_increment = true;
16683                implied_not_null = true;
16684                ColumnTypeName::BigInt
16685            }
16686            // MySQL flavours we accept by aliasing to the closest SPG
16687            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
16688            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
16689            // 24-bit) → INT. UNSIGNED modifiers are consumed below
16690            // without semantic effect.
16691            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
16692            // PG's internal type names; pg_dump and hand-written PG schemas
16693            // use them interchangeably with smallint / int / bigint (the cast
16694            // path already accepted them, only the column grammar didn't).
16695            "smallint" | "int2" => {
16696                // v7.14.0 — MySQL display-width on integers
16697                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
16698                // parenthesised number is purely cosmetic — it
16699                // doesn't change storage. Accept + discard.
16700                self.consume_optional_paren_size();
16701                ColumnTypeName::SmallInt
16702            }
16703            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
16704            // canonical encoding for BOOLEAN. Every MySQL driver
16705            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
16706            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
16707            // 4.3 SPG classified TINYINT(1) as SmallInt, which
16708            // gave the customer i16-shaped values where the app
16709            // expected bool — a Tier-A silent type drift on
16710            // mysqldump restores. Now: `TINYINT(1)` → Bool;
16711            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
16712            // stay SmallInt (the legacy width-agnostic path).
16713            "tinyint" => {
16714                let width = self.peek_optional_paren_size_value();
16715                self.consume_optional_paren_size();
16716                if width == Some(1) {
16717                    ColumnTypeName::Bool
16718                } else {
16719                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
16720                    // lost width so the write path can enforce -128..127.
16721                    if self.mysql_dialect {
16722                        mysql_int_width = Some(MysqlIntWidth::Tiny);
16723                    }
16724                    ColumnTypeName::SmallInt
16725                }
16726            }
16727            "mediumint" => {
16728                self.consume_optional_paren_size();
16729                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
16730                if self.mysql_dialect {
16731                    mysql_int_width = Some(MysqlIntWidth::Medium);
16732                }
16733                ColumnTypeName::Int
16734            }
16735            "int" | "integer" | "int4" => {
16736                self.consume_optional_paren_size();
16737                ColumnTypeName::Int
16738            }
16739            "bigint" | "int8" => {
16740                self.consume_optional_paren_size();
16741                ColumnTypeName::BigInt
16742            }
16743            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
16744            // (mailrs round-5 G6). Consume the optional `PRECISION`
16745            // tail when the type keyword was `double` / `DOUBLE`.
16746            //
16747            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
16748            // FLOAT". `FLOAT(p)` picks the width the way PG does:
16749            // p in 1..=24 is real, 25..=53 is double precision, and
16750            // anything else is an error.
16751            "float" | "double" | "real" => {
16752                if ty_ident.eq_ignore_ascii_case("double")
16753                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
16754                {
16755                    self.advance();
16756                }
16757                if ty_ident.eq_ignore_ascii_case("real") {
16758                    // v7.39 (round 274) — the two dialects genuinely
16759                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
16760                    // synonym for DOUBLE (8-byte). Round 269 made REAL
16761                    // 32-bit globally and thereby narrowed the stored
16762                    // precision of every MySQL REAL column.
16763                    if self.mysql_dialect {
16764                        ColumnTypeName::Float
16765                    } else {
16766                        ColumnTypeName::Real
16767                    }
16768                } else if self.mysql_dialect
16769                    && matches!(self.peek(), Token::LParen)
16770                    && self.peek_paren_has_comma()
16771                {
16772                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
16773                    // display form (`FLOAT(10,2)`), which PG has no
16774                    // equivalent of. It was `syntax error at or near ","`,
16775                    // so the whole CREATE failed.
16776                    //
16777                    // v7.39.2 — the guard said `float` while the comment
16778                    // said both, so `DOUBLE(10,2)` — which every legacy
16779                    // MySQL schema uses for money — still failed the
16780                    // whole CREATE with `syntax error at or near "("`.
16781                    // Measured on 9.7.2: both forms are accepted, and the
16782                    // digits are NOT a display hint, they round on write
16783                    // (3.14159265358979 into either stores 3.14). The
16784                    // rounding is recorded as a residual; accepting the
16785                    // syntax and keeping the width is the half this
16786                    // change makes.
16787                    // v7.39.3 — keep the pair. The digits are not a
16788                    // display hint: MySQL 9.7.2 ROUNDS on write and
16789                    // refuses a value wider than `m` (errno 1264), so a
16790                    // column declared for money held more precision here
16791                    // than its schema said.
16792                    let (m, d) = self.parse_optional_numeric_params()?;
16793                    mysql_float_md = Some((
16794                        u8::try_from(m).unwrap_or(u8::MAX),
16795                        u8::try_from(d.max(0)).unwrap_or(u8::MAX),
16796                    ));
16797                    if ty_ident.eq_ignore_ascii_case("float") {
16798                        ColumnTypeName::Real
16799                    } else {
16800                        ColumnTypeName::Float
16801                    }
16802                } else if ty_ident.eq_ignore_ascii_case("float")
16803                    && matches!(self.peek(), Token::LParen)
16804                {
16805                    // PG words the two bounds differently, and
16806                    // parse_paren_size already rejects a zero.
16807                    let p = self.parse_paren_size("FLOAT")?;
16808                    if p > 53 {
16809                        return Err(self.err(String::from(
16810                            "precision for type float must be less than 54 bits",
16811                        )));
16812                    }
16813                    if p <= 24 {
16814                        ColumnTypeName::Real
16815                    } else {
16816                        ColumnTypeName::Float
16817                    }
16818                } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
16819                    // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
16820                    // eight (it is `float8`'s spelling there). SPG used
16821                    // PG's for both, so a MySQL FLOAT column silently
16822                    // kept more precision than MySQL does — measured,
16823                    // 3.14159265358979 comes back as 3.14159 there and
16824                    // came back whole here — and reported itself as
16825                    // `double` to every reflection.
16826                    //
16827                    // This is the mirror of the REAL split above: the
16828                    // two dialects disagree about which spelling means
16829                    // which width, and one of them was already honoured.
16830                    ColumnTypeName::Real
16831                } else {
16832                    ColumnTypeName::Float
16833                }
16834            }
16835            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
16836            "float4" => ColumnTypeName::Real,
16837            "float8" => ColumnTypeName::Float,
16838            "text" => ColumnTypeName::Text,
16839            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
16840            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
16841            // real MySQL schema and NONE of them existed: the CREATE
16842            // failed outright with `type "blob" does not exist`, so the
16843            // table was never made. The sizes differ only in MySQL's
16844            // maximum length, which SPG does not cap, so they collapse
16845            // onto TEXT and BYTEA the way the unsized spellings do.
16846            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
16847            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
16848            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
16849            // enforce, consumed so the declaration parses.
16850            "varbinary" | "binary" => {
16851                self.consume_optional_paren_size();
16852                ColumnTypeName::Bytes
16853            }
16854            "name" => ColumnTypeName::Name,
16855            "xid" => ColumnTypeName::Xid,
16856            "oid" => ColumnTypeName::Oid,
16857            "xid8" => ColumnTypeName::Xid8,
16858            "bool" | "boolean" => ColumnTypeName::Bool,
16859            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
16860            // an unbounded `character varying`, which the arm below has always
16861            // read as text. Only the short spelling demanded a length, so
16862            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
16863            // there is — failed on `VARCHAR type requires (N)` while the long
16864            // spelling of the same thing was accepted. The same asymmetry
16865            // round 613 closed on the CAST side, here on the DDL side.
16866            "varchar" => {
16867                if matches!(self.peek(), Token::LParen) {
16868                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16869                } else {
16870                    ColumnTypeName::Text
16871                }
16872            }
16873            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
16874            // `character` below (SQL standard).
16875            "char" => {
16876                if matches!(self.peek(), Token::LParen) {
16877                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16878                } else {
16879                    ColumnTypeName::Char(1)
16880                }
16881            }
16882            // pg_dump's canonical spellings: `character varying(n)` = varchar,
16883            // `character(n)` = char, bare `character` = char(1). Unbounded
16884            // `character varying` maps to text.
16885            "character" => {
16886                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
16887                    self.advance();
16888                    if matches!(self.peek(), Token::LParen) {
16889                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
16890                    } else {
16891                        ColumnTypeName::Text
16892                    }
16893                } else if matches!(self.peek(), Token::LParen) {
16894                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
16895                } else {
16896                    ColumnTypeName::Char(1)
16897                }
16898            }
16899            "vector" => {
16900                let dim = self.parse_paren_size("VECTOR")?;
16901                let encoding = self.parse_optional_vector_encoding()?;
16902                ColumnTypeName::Vector { dim, encoding }
16903            }
16904            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
16905            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
16906            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
16907            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
16908            // DECIMAL(10,2))` — how nearly every money column is written,
16909            // in either dialect — was a syntax error and the table was
16910            // never created. `FIXED` is MySQL's alias alone, so it is
16911            // taken only in that dialect.
16912            "numeric" | "decimal" | "dec" => {
16913                let (precision, scale) = self.parse_optional_numeric_params()?;
16914                ColumnTypeName::Numeric(precision, scale)
16915            }
16916            "fixed" if self.mysql_dialect => {
16917                let (precision, scale) = self.parse_optional_numeric_params()?;
16918                ColumnTypeName::Numeric(precision, scale)
16919            }
16920            "date" => ColumnTypeName::Date,
16921            // MySQL's `DATETIME` is the same domain as standard
16922            // `TIMESTAMP` — accept both spellings.
16923            "timestamp" | "datetime" => {
16924                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
16925                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
16926                // TIME ZONE` clause, so consume it first.
16927                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
16928                // (it truncates on write and pads on render), so capture it;
16929                // a bare spelling means precision 0 there. PG stores µs always
16930                // and keeps `None`.
16931                let n = self.take_optional_paren_size();
16932                if self.mysql_dialect {
16933                    mysql_fsp = Some(n.unwrap_or(0).min(6));
16934                    // v7.39.2 — remember WHICH spelling was written.
16935                    // MySQL and MariaDB keep `timestamp` and `datetime`
16936                    // apart everywhere a client can read the type back,
16937                    // and SPG reported `datetime` for both — so a dump
16938                    // and reload silently changed the column's declared
16939                    // type, and MySQL's TIMESTAMP is not DATETIME (a
16940                    // different range, and UTC conversion on the way in
16941                    // and out).
16942                    mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
16943                }
16944                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
16945                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
16946                // the full form. SPG canonicalises:
16947                //   - WITH TIME ZONE    → Timestamptz
16948                //   - WITHOUT TIME ZONE → Timestamp
16949                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
16950                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16951                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16952                {
16953                    self.advance(); // WITH
16954                    self.advance(); // TIME
16955                    self.advance(); // ZONE
16956                    ColumnTypeName::Timestamptz
16957                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
16958                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
16959                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
16960                {
16961                    self.advance(); // WITHOUT
16962                    self.advance(); // TIME
16963                    self.advance(); // ZONE
16964                    ColumnTypeName::Timestamp
16965                } else {
16966                    // A second `(precision)` cannot legally follow, but the
16967                    // old grammar tolerated it; keep that tolerance.
16968                    self.consume_optional_paren_size();
16969                    ColumnTypeName::Timestamp
16970                }
16971            }
16972            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
16973            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
16974            // only PG-wire OID differs.
16975            "timestamptz" => {
16976                self.consume_optional_paren_size();
16977                ColumnTypeName::Timestamptz
16978            }
16979            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
16980            // validation. We accept the JSONB spelling too because
16981            // most PG clients default to it; SPG doesn't distinguish
16982            // the two (no path-operator perf advantage to model).
16983            "json" => ColumnTypeName::Json,
16984            "jsonb" => ColumnTypeName::Jsonb,
16985            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
16986            // surface here. Same storage shape; mapping happens at
16987            // the engine side via the ColumnTypeName → DataType
16988            // resolver. Literal forms are handled at coerce_value
16989            // time so the lexer stays untouched.
16990            "bytea" | "bytes" => ColumnTypeName::Bytes,
16991            // v7.17.0 Phase 7 — PG network address types
16992            // v7.17.0 had a Text-backed fallback here for
16993            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
16994            // each to a first-class type; the keywords are
16995            // bound below in the ζ-A block.
16996            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
16997            // The actual `to_tsvector` / `@@` / `ts_rank` surface
16998            // arrives in v7.12.1+; the type itself loads here so
16999            // mailrs's `scripts/init-schema.sql` runs unmodified.
17000            "tsvector" => ColumnTypeName::TsVector,
17001            "tsquery" => ColumnTypeName::TsQuery,
17002            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
17003            // surface for Django / Rails / Hibernate's default
17004            // PK pattern.
17005            "uuid" => ColumnTypeName::Uuid,
17006            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
17007            // Storage = three-field {months, days, micros}, catalog
17008            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
17009            // line `INTERVAL` was parser-rejected at CREATE TABLE.
17010            "interval" => {
17011                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
17012                // SECOND` and an optional `(p)` precision. SPG stores the full
17013                // {months,days,micros}; consume + ignore the qualifier/precision.
17014                while matches!(self.peek(), Token::To)
17015                    || matches!(self.peek(), Token::Ident(s) if matches!(
17016                        s.to_ascii_lowercase().as_str(),
17017                        "year" | "month" | "day" | "hour" | "minute" | "second"
17018                    ))
17019                {
17020                    self.advance();
17021                }
17022                self.consume_optional_paren_size();
17023                ColumnTypeName::Interval
17024            }
17025            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
17026            // i64 microseconds since 00:00:00. Wire OID 1083.
17027            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
17028            "time" => {
17029                // v7.39 (round 424) — MySQL TIME carries a semantic
17030                // fractional-seconds precision, bare meaning 0.
17031                let n = self.take_optional_paren_size();
17032                if self.mysql_dialect {
17033                    mysql_fsp = Some(n.unwrap_or(0).min(6));
17034                }
17035                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17036                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17037                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17038                {
17039                    self.advance();
17040                    self.advance();
17041                    self.advance();
17042                    ColumnTypeName::TimeTz
17043                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17044                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17045                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17046                {
17047                    self.advance();
17048                    self.advance();
17049                    self.advance();
17050                    ColumnTypeName::Time
17051                } else {
17052                    ColumnTypeName::Time
17053                }
17054            }
17055            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17056            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17057            "year" => ColumnTypeName::Year,
17058            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17059            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17060            "timetz" => ColumnTypeName::TimeTz,
17061            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17062            // Wire OID 790.
17063            "money" => ColumnTypeName::Money,
17064            // v7.17.0 Phase 3.P0-38 — PG range types.
17065            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17066            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17067            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17068            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17069            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17070            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17071            // v7.37.5 δ — PG 14+ multirange keywords.
17072            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17073            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17074            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17075            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17076            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17077            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17078            // v7.37.5 ε — PG geometry scalar keywords.
17079            "point" => ColumnTypeName::Point,
17080            "lseg" => ColumnTypeName::Lseg,
17081            "path" => ColumnTypeName::Path,
17082            "box" => ColumnTypeName::PgBox,
17083            "polygon" => ColumnTypeName::Polygon,
17084            "line" => ColumnTypeName::Line,
17085            "circle" => ColumnTypeName::Circle,
17086            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17087            "inet" => ColumnTypeName::Inet,
17088            "cidr" => ColumnTypeName::Cidr,
17089            "macaddr" => ColumnTypeName::Macaddr,
17090            "macaddr8" => ColumnTypeName::Macaddr8,
17091            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17092            // width in the value, so the optional `(N)` typmod is accepted and
17093            // ignored (the column stores whatever width it's given).
17094            "bit" => {
17095                let varying = matches!(
17096                    self.peek(),
17097                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17098                );
17099                if varying {
17100                    self.advance();
17101                }
17102                // v7.39 (round 281) — the length used to be parsed and
17103                // dropped, so `bit(3)` accepted a five-bit string.
17104                let n = if matches!(self.peek(), Token::LParen) {
17105                    self.parse_paren_size("BIT")?
17106                } else {
17107                    0
17108                };
17109                if varying {
17110                    ColumnTypeName::BitVarying(n)
17111                } else {
17112                    ColumnTypeName::Bit(n)
17113                }
17114            }
17115            "varbit" => {
17116                let n = if matches!(self.peek(), Token::LParen) {
17117                    self.parse_paren_size("VARBIT")?
17118                } else {
17119                    0
17120                };
17121                ColumnTypeName::BitVarying(n)
17122            }
17123            "xml" => ColumnTypeName::Xml,
17124            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17125            "hstore" => ColumnTypeName::Hstore,
17126            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17127            // `ENUM('a','b','c')`. Storage is TEXT; the value
17128            // list lands on `inline_enum_variants` for the
17129            // engine to validate INSERT cells against. Empty
17130            // value list is a parse error (matches MySQL).
17131            "enum" => {
17132                // Expect the opening `(`.
17133                if !matches!(self.peek(), Token::LParen) {
17134                    return Err(self.err(alloc::format!(
17135                        "expected '(' after ENUM, got {:?}",
17136                        self.peek()
17137                    )));
17138                }
17139                self.advance();
17140                let mut variants: Vec<String> = Vec::new();
17141                loop {
17142                    match self.advance() {
17143                        Token::String(s) => variants.push(s),
17144                        other => {
17145                            return Err(self.err(alloc::format!(
17146                                "ENUM(...) expects string literal variants, got {other:?}"
17147                            )));
17148                        }
17149                    }
17150                    match self.peek() {
17151                        Token::Comma => {
17152                            self.advance();
17153                            continue;
17154                        }
17155                        Token::RParen => {
17156                            self.advance();
17157                            break;
17158                        }
17159                        other => {
17160                            return Err(self.err(alloc::format!(
17161                                "expected ',' or ')' in ENUM(...), got {other:?}"
17162                            )));
17163                        }
17164                    }
17165                }
17166                if variants.is_empty() {
17167                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
17168                }
17169                inline_enum_variants = Some(variants);
17170                // Storage is plain TEXT; the variant list lives on
17171                // the ColumnSchema side.
17172                ColumnTypeName::Text
17173            }
17174            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17175            // `SET('a','b','c')`. Same parse shape as ENUM;
17176            // semantics differ (subset rather than pick-one).
17177            "set" => {
17178                if !matches!(self.peek(), Token::LParen) {
17179                    return Err(self.err(alloc::format!(
17180                        "expected '(' after SET, got {:?}",
17181                        self.peek()
17182                    )));
17183                }
17184                self.advance();
17185                let mut variants: Vec<String> = Vec::new();
17186                loop {
17187                    match self.advance() {
17188                        Token::String(s) => variants.push(s),
17189                        other => {
17190                            return Err(self.err(alloc::format!(
17191                                "SET(...) expects string literal variants, got {other:?}"
17192                            )));
17193                        }
17194                    }
17195                    match self.peek() {
17196                        Token::Comma => {
17197                            self.advance();
17198                            continue;
17199                        }
17200                        Token::RParen => {
17201                            self.advance();
17202                            break;
17203                        }
17204                        other => {
17205                            return Err(self.err(alloc::format!(
17206                                "expected ',' or ')' in SET(...), got {other:?}"
17207                            )));
17208                        }
17209                    }
17210                }
17211                if variants.is_empty() {
17212                    return Err(self.err("SET(...) must declare at least one variant".into()));
17213                }
17214                inline_set_variants = Some(variants);
17215                ColumnTypeName::Text
17216            }
17217            _other => {
17218                // v7.17.0 Phase 1.4 — unknown ident → defer
17219                // resolution to the engine. Stored as Text in
17220                // ColumnTypeName + the original name carried as
17221                // `user_type_ref` so CREATE TABLE can look up
17222                // user-defined enum / domain types.
17223                user_type_ref = Some(ty_ident.clone());
17224                ColumnTypeName::Text
17225            }
17226        };
17227        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17228        // right after the type keyword. Pre-4.4 SPG consumed +
17229        // discarded the keyword, leaving a customer column
17230        // declared `id INT UNSIGNED NOT NULL` silently accepting
17231        // negative values — a Tier-A correctness drift where
17232        // application invariants (auto-increment-IDs never
17233        // negative) silently broke on cutover. Now: capture as
17234        // a column flag, persist on the schema, enforce at
17235        // INSERT / UPDATE time.
17236        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17237        {
17238            self.advance();
17239            true
17240        } else {
17241            false
17242        };
17243        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17244        // `<type> COLLATE <name>` post-fixes on text columns. SPG
17245        // stores text as UTF-8 always so CHARACTER SET is still a
17246        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17247        // name: it gets classified into a `Collation` variant the
17248        // engine consults at WHERE-eval time. PG `default` /
17249        // `pg_catalog.default` / `C` / `POSIX` collations all
17250        // resolve to `Binary` (the prior behaviour); `_ci` /
17251        // `case_insensitive` / `nocase` shift to CaseInsensitive.
17252        // The schema-qualifier form (`pg_catalog.default`) lexes
17253        // as `Ident '.' Ident` — peek for the `.` and consume both
17254        // halves so it's treated as one collation name. PG's
17255        // `IDENT.IDENT` collation form (which can appear here) is
17256        // resolved by Collation::from_collation_name on the bare
17257        // identifier after the dot.
17258        let mut collation = Collation::Binary;
17259        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17260        // clause was written. The engine needs this to tell an explicit
17261        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17262        // clause at all: both resolve to `Collation::Binary`, but under the
17263        // MySQL dialect the latter takes the folding default collation.
17264        let mut collation_explicit = false;
17265        let mut collation_name: Option<alloc::string::String> = None;
17266        loop {
17267            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17268                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17269            {
17270                self.advance(); // CHARACTER
17271                self.advance(); // SET
17272                if matches!(
17273                    self.peek(),
17274                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17275                ) {
17276                    self.advance();
17277                }
17278                continue;
17279            }
17280            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17281                self.advance(); // COLLATE
17282                // Accept Ident / QuotedIdent / String AND the
17283                // keyword-tokenised `Default` (PG `pg_catalog.default`
17284                // and bare `DEFAULT` collation names — `default` is a
17285                // reserved word so the lexer hands back Token::Default
17286                // not Token::Ident).
17287                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17288                    match this.peek().clone() {
17289                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17290                            this.advance();
17291                            Some(s)
17292                        }
17293                        Token::Default => {
17294                            this.advance();
17295                            Some(alloc::string::String::from("default"))
17296                        }
17297                        _ => None,
17298                    }
17299                };
17300                let raw = if let Some(head) = read_collation_atom(self) {
17301                    // Schema-qualified PG form: `pg_catalog.default`.
17302                    if matches!(self.peek(), Token::Dot) {
17303                        self.advance();
17304                        let tail = read_collation_atom(self).unwrap_or_default();
17305                        alloc::format!("{head}.{tail}")
17306                    } else {
17307                        head
17308                    }
17309                } else {
17310                    alloc::string::String::new()
17311                };
17312                if !raw.is_empty() {
17313                    collation_explicit = true;
17314                    // v7.39 (round 676) — keep the name too. The enum below
17315                    // folds C / POSIX / en_US / default into one value, and
17316                    // `pg_attribute.attcollation` has to tell them apart.
17317                    // The schema qualifier goes: PG's `pg_catalog.default`
17318                    // and a bare `default` name the same collation.
17319                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17320                    // encoding suffix. Round 676 used `rsplit('.')` for
17321                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17322                    // PG writes `pg_catalog.default` (qualifier) and
17323                    // `en_US.utf8` (locale + encoding) with the same
17324                    // separator. Only `pg_catalog.` is a qualifier, and it
17325                    // is the only one PG's own dumps emit.
17326                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17327                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17328                    collation_name = Some(alloc::string::String::from(bare));
17329                    let parsed = Collation::from_collation_name(&raw);
17330                    // Last COLLATE clause wins, but `Binary` from a
17331                    // bare keyword like `default` should not
17332                    // silently downgrade a stronger one set earlier
17333                    // on the same column. v7.17 only ships one
17334                    // non-Binary variant so a simple OR is enough.
17335                    if parsed != Collation::Binary {
17336                        collation = parsed;
17337                    }
17338                }
17339                continue;
17340            }
17341            break;
17342        }
17343        // v7.10.10 — postfix `[]` widens the base type to its array
17344        // type. PG accepts `TYPE[]` after any base type and so does
17345        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17346        // all through; the old "only TEXT[]" note was stale).
17347        if matches!(self.peek(), Token::LBracket) {
17348            self.advance();
17349            if !matches!(self.peek(), Token::RBracket) {
17350                return Err(self.err(alloc::format!(
17351                    "TEXT[] takes no dimension; got {:?}",
17352                    self.peek()
17353                )));
17354            }
17355            self.advance();
17356            // v7.11.13 — widened to INT[] and BIGINT[] in addition
17357            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17358            // still error here.
17359            ty = match ty {
17360                ColumnTypeName::Text => ColumnTypeName::TextArray,
17361                ColumnTypeName::Int => ColumnTypeName::IntArray,
17362                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17363                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17364                // `[]` grammar. Wire OID 1187.
17365                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17366                // v7.37.5 γ — full PG array-of-scalar family.
17367                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17368                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17369                ColumnTypeName::Float => ColumnTypeName::FloatArray,
17370                // NUMERIC(p, s) loses its precision params at the
17371                // array level (matches PG: `NUMERIC[]` is untyped,
17372                // per-element precision flows through values).
17373                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17374                ColumnTypeName::Date => ColumnTypeName::DateArray,
17375                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17376                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17377                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17378                ColumnTypeName::Json => ColumnTypeName::JsonArray,
17379                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17380                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17381                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17382                // the array level (matches PG semantics where the
17383                // element precision is per-row, not column-wide).
17384                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17385                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17386                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17387                // follow-up.
17388                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17389                other => {
17390                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17391                }
17392            };
17393            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17394            // for INT/TEXT/BIGINT. Anything else is an error.
17395            if matches!(self.peek(), Token::LBracket) {
17396                self.advance();
17397                if !matches!(self.peek(), Token::RBracket) {
17398                    return Err(self.err(alloc::format!(
17399                        "TYPE[][] second dimension takes no size; got {:?}",
17400                        self.peek()
17401                    )));
17402                }
17403                self.advance();
17404                ty = match ty {
17405                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17406                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17407                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17408                    // v7.39 (read01 round 75) — bool[][].
17409                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17410                    other => {
17411                        return Err(self.err(alloc::format!(
17412                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17413                             TEXT[][] only; got {other:?}"
17414                        )));
17415                    }
17416                };
17417            }
17418        }
17419        Ok((
17420            ty,
17421            implied_auto_increment,
17422            implied_not_null,
17423            user_type_ref,
17424            collation,
17425            collation_explicit,
17426            collation_name,
17427            is_unsigned,
17428            inline_enum_variants,
17429            inline_set_variants,
17430            mysql_int_width,
17431            mysql_fsp,
17432            mysql_declared_timestamp,
17433            mysql_float_md,
17434        ))
17435    }
17436
17437    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17438        // v7.20 — PG reserves the table-constraint keywords, so a
17439        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17440        // malformed constraint clause (e.g. `UNIQUE a` missing its
17441        // parens), not a column named "unique". Since v7.17's
17442        // unknown-type leniency (`user_type_ref`) such a clause
17443        // would otherwise parse as a column with a user-defined
17444        // type — silently accepting invalid DDL. Quoted
17445        // identifiers ("unique" / `unique`) remain valid names.
17446        if let Token::Ident(s) = self.peek()
17447            && [
17448                "unique",
17449                "primary",
17450                "foreign",
17451                "constraint",
17452                "check",
17453                "references",
17454                "exclude",
17455            ]
17456            .iter()
17457            .any(|kw| s.eq_ignore_ascii_case(kw))
17458        {
17459            return Err(self.err(alloc::format!(
17460                "unexpected reserved keyword '{s}' at start of column definition \
17461                 (malformed table constraint?)"
17462            )));
17463        }
17464        let name_tok = self.pos;
17465        let name = self.expect_ident_like()?;
17466        // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17467        // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17468        // information_schema, and in SHOW CREATE (measured). SPG folded
17469        // an unquoted name, so a table restored from a dump reported
17470        // names the application had never written.
17471        //
17472        // The written form comes back from the source span, which only
17473        // the MySQL dialect keeps. The span runs to the START of the
17474        // next token, so a comment or unusual spacing between them
17475        // arrives with it — hence the check that what came back is the
17476        // same identifier. It is not decoration: without it,
17477        // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17478        // `MyCol /* c */`.
17479        let name = self
17480            .source_span(name_tok, name_tok)
17481            .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17482            .filter(|raw| raw.eq_ignore_ascii_case(&name))
17483            .map_or(name, alloc::string::String::from);
17484        let (
17485            ty,
17486            implied_auto_increment,
17487            implied_not_null,
17488            user_type_ref,
17489            collation,
17490            collation_explicit,
17491            collation_name,
17492            is_unsigned,
17493            inline_enum_variants,
17494            inline_set_variants,
17495            mysql_int_width,
17496            mysql_fsp,
17497            mysql_declared_timestamp,
17498            mysql_float_md,
17499        ) = self.parse_type_with_implied_flags()?;
17500        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17501        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17502        // each at most once.
17503        let mut default: Option<Expr> = None;
17504        let mut nullable = !implied_not_null;
17505        let mut nullability_seen = implied_not_null;
17506        let mut auto_increment = implied_auto_increment;
17507        let mut is_primary_key = false;
17508        let mut is_unique = false;
17509        let mut unique_nulls_not_distinct = false;
17510        let mut constraint_deferrable = false;
17511        let mut constraint_initially_deferred = false;
17512        let mut check: Option<Expr> = None;
17513        let mut on_update_runtime: Option<Expr> = None;
17514        let mut generated_stored_expr: Option<Box<Expr>> = None;
17515        let mut identity_always = false;
17516        loop {
17517            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17518            // not-null constraints by name and pg_dump emits them
17519            // inline: `id bigint CONSTRAINT contacts_id_not_null1
17520            // NOT NULL`. Accept and discard the name; whatever
17521            // constraint follows is parsed by the arms below.
17522            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17523                // v7.39 (round 308, V29) — a name on an inline
17524                // REFERENCES belongs to the FOREIGN KEY, and the caller
17525                // (`parse_column_def_with_fk`) is what builds it, so
17526                // leave the whole clause for it. Dropping the name here
17527                // is what made `CONSTRAINT fk_a REFERENCES …` come back
17528                // as the synthesised `c_pid_fkey` — which then could
17529                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17530                // `advance()` takes tokens by `mem::replace`, so there
17531                // is no rewinding once consumed.
17532                if matches!(
17533                    self.tokens.get(self.pos + 2),
17534                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17535                ) {
17536                    break;
17537                }
17538                self.advance();
17539                let _name = self.expect_ident_like()?;
17540                continue;
17541            }
17542            // v7.39 (round 379) — MySQL's SHORT generated-column form
17543            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17544            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17545            // below), but hand-written schemas and app migrations use this.
17546            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17547            // SPG computes-and-stores either way, like the long form.
17548            if matches!(self.peek(), Token::As) {
17549                self.advance();
17550                if !matches!(self.peek(), Token::LParen) {
17551                    return Err(self.err(alloc::format!(
17552                        "expected '(' after AS in a generated column, got {:?}",
17553                        self.peek()
17554                    )));
17555                }
17556                self.advance();
17557                let expr = self.parse_expr(0)?;
17558                if !matches!(self.peek(), Token::RParen) {
17559                    return Err(self.err(alloc::format!(
17560                        "expected ')' after AS (<expr>), got {:?}",
17561                        self.peek()
17562                    )));
17563                }
17564                self.advance();
17565                if matches!(self.peek(), Token::Ident(s)
17566                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17567                {
17568                    self.advance();
17569                }
17570                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17571                continue;
17572            }
17573            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17574            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17575            // the modern replacement for SERIAL in hand-written
17576            // schemas). Both flavours map onto the auto-increment
17577            // machinery — SPG's serial semantics ≈ BY DEFAULT;
17578            // ALWAYS's reject-explicit-values nuance is documented
17579            // leniency. Generated EXPRESSION columns
17580            // (`AS (expr) STORED`) are not supported: error loudly
17581            // instead of silently storing NULLs.
17582            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17583                self.advance();
17584                let mut saw_generated_always = false;
17585                match self.peek().clone() {
17586                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17587                        self.advance();
17588                        saw_generated_always = true;
17589                    }
17590                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17591                        self.advance();
17592                        if !matches!(self.peek(), Token::Default) {
17593                            return Err(self.err(alloc::format!(
17594                                "expected DEFAULT after GENERATED BY, got {:?}",
17595                                self.peek()
17596                            )));
17597                        }
17598                        self.advance();
17599                    }
17600                    other => {
17601                        return Err(self.err(alloc::format!(
17602                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17603                        )));
17604                    }
17605                }
17606                if !matches!(self.peek(), Token::As) {
17607                    return Err(self.err(alloc::format!(
17608                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17609                        self.peek()
17610                    )));
17611                }
17612                self.advance();
17613                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17614                // ( <expr> ) STORED` stored computed-column. The
17615                // expression is captured for the engine to recompute
17616                // on every INSERT / UPDATE. v7.37.7 accepts the
17617                // STORED keyword only; PG also has VIRTUAL, which
17618                // v7.37.7 carves out (sentori only uses STORED).
17619                if matches!(self.peek(), Token::LParen) {
17620                    self.advance();
17621                    let expr = self.parse_expr(0)?;
17622                    if !matches!(self.peek(), Token::RParen) {
17623                        return Err(self.err(alloc::format!(
17624                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17625                            self.peek()
17626                        )));
17627                    }
17628                    self.advance();
17629                    let stored = match self.peek() {
17630                        Token::Ident(s) | Token::QuotedIdent(s)
17631                            if s.eq_ignore_ascii_case("stored") =>
17632                        {
17633                            self.advance();
17634                            true
17635                        }
17636                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17637                        // generated columns. SPG computes them on write and
17638                        // persists like STORED; the two are observably
17639                        // identical for query results (the value, recompute
17640                        // on base-column change, and NOT NULL enforcement all
17641                        // match), so a PG 18 schema/dump using VIRTUAL loads
17642                        // and behaves correctly. The compute-on-read storage
17643                        // saving is an invisible internal difference.
17644                        Token::Ident(s) | Token::QuotedIdent(s)
17645                            if s.eq_ignore_ascii_case("virtual") =>
17646                        {
17647                            self.advance();
17648                            false
17649                        }
17650                        other => {
17651                            return Err(self.err(alloc::format!(
17652                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
17653                                 got {other:?}"
17654                            )));
17655                        }
17656                    };
17657                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
17658                    generated_stored_expr = Some(Box::new(expr));
17659                    continue;
17660                }
17661                self.expect_keyword_ident("identity")?;
17662                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
17663                // consume the balanced parens and discard (SPG's
17664                // auto-increment is max+1-scan based).
17665                if matches!(self.peek(), Token::LParen) {
17666                    let mut depth = 0usize;
17667                    loop {
17668                        match self.advance() {
17669                            Token::LParen => depth += 1,
17670                            Token::RParen => {
17671                                depth -= 1;
17672                                if depth == 0 {
17673                                    break;
17674                                }
17675                            }
17676                            Token::Eof => {
17677                                return Err(self.err(
17678                                    "unterminated sequence-options parens after IDENTITY".into(),
17679                                ));
17680                            }
17681                            _ => {}
17682                        }
17683                    }
17684                }
17685                auto_increment = true;
17686                // v7.38 (read01) — remember the ALWAYS flavour so the engine
17687                // can reject explicit non-DEFAULT INSERT values (unless
17688                // OVERRIDING SYSTEM VALUE) the way PG does.
17689                identity_always = saw_generated_always;
17690                // PG identity columns are implicitly NOT NULL.
17691                nullable = false;
17692                continue;
17693            }
17694            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
17695            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
17696            // is accepted today. The "ON" token is an Ident
17697            // (not reserved) — peek before consuming.
17698            if matches!(self.peek(), Token::On)
17699                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
17700            {
17701                self.advance(); // ON
17702                self.advance(); // update
17703                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
17704                let next = self.peek().clone();
17705                match next {
17706                    Token::Ident(s) | Token::QuotedIdent(s)
17707                        if s.eq_ignore_ascii_case("current_timestamp") =>
17708                    {
17709                        self.advance();
17710                        // Optional `(N)` precision.
17711                        if matches!(self.peek(), Token::LParen) {
17712                            self.advance();
17713                            if !matches!(self.peek(), Token::Integer(_)) {
17714                                return Err(self.err(alloc::format!(
17715                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
17716                                    self.peek()
17717                                )));
17718                            }
17719                            self.advance();
17720                            if !matches!(self.peek(), Token::RParen) {
17721                                return Err(self.err(alloc::format!(
17722                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
17723                                    self.peek()
17724                                )));
17725                            }
17726                            self.advance();
17727                        }
17728                        on_update_runtime = Some(Expr::FunctionCall {
17729                            name: "now".into(),
17730                            args: Vec::new(),
17731                        });
17732                        continue;
17733                    }
17734                    other => {
17735                        return Err(self.err(alloc::format!(
17736                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
17737                        )));
17738                    }
17739                }
17740            }
17741            if matches!(self.peek(), Token::Default) {
17742                if default.is_some() {
17743                    return Err(self.err("DEFAULT specified twice".into()));
17744                }
17745                self.advance();
17746                default = Some(self.parse_expr(0)?);
17747                continue;
17748            }
17749            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
17750            // token with NOT NULL and sits EARLIER in the loop than the
17751            // deferrability arm, so without the lookahead it was reported as
17752            // "NOT NULL specified twice" (or "expected NULL after NOT").
17753            if matches!(self.peek(), Token::Not)
17754                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
17755            {
17756                // NOT DEFERRABLE — explicit immediate; nothing to carry.
17757                self.consume_optional_deferrable_clauses()?;
17758                continue;
17759            }
17760            if matches!(self.peek(), Token::Not) {
17761                if nullability_seen {
17762                    return Err(self.err("NOT NULL specified twice".into()));
17763                }
17764                self.advance();
17765                if !matches!(self.peek(), Token::Null) {
17766                    return Err(self.err(format!(
17767                        "expected NULL after NOT in column def, got {:?}",
17768                        self.peek()
17769                    )));
17770                }
17771                self.advance();
17772                nullable = false;
17773                nullability_seen = true;
17774                continue;
17775            }
17776            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
17777            // "this column is nullable" marker (the default in
17778            // standard SQL anyway). mysqldump emits it routinely
17779            // (`col TYPE NULL DEFAULT NULL` for nullable
17780            // timestamps etc). Accept + no-op.
17781            if matches!(self.peek(), Token::Null) {
17782                if nullability_seen && !nullable {
17783                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
17784                    // sentence, PG18-measured (the table name is the
17785                    // caller's; the column half is exact).
17786                    return Err(self.err(alloc::format!(
17787                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
17788                    )));
17789                }
17790                self.advance();
17791                nullable = true;
17792                nullability_seen = true;
17793                continue;
17794            }
17795            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
17796            // arrives as a bare Ident. Match either, case-insensitive.
17797            if let Token::Ident(s) = self.peek()
17798                && (s.eq_ignore_ascii_case("auto_increment")
17799                    || s.eq_ignore_ascii_case("autoincrement"))
17800            {
17801                if auto_increment {
17802                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
17803                }
17804                self.advance();
17805                auto_increment = true;
17806                continue;
17807            }
17808            // v7.9.13 — inline `PRIMARY KEY` column constraint
17809            // (mailrs F1). Implies `NOT NULL`. The engine creates
17810            // a BTree index for the PK column at CREATE TABLE time
17811            // so FK parent-side index lookups resolve.
17812            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
17813            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
17814            // spelling was a parse error, so a pg_dump carrying one stopped
17815            // mid-restore. The clauses are consumed by the same helper the FK
17816            // path has used since round 288 and recorded nowhere: SPG enforces
17817            // the constraint IMMEDIATELY either way, which fails earlier than
17818            // PG inside a transaction that violates-then-repairs — a refusal,
17819            // not a wrong answer. True deferral is the open remainder of F08.
17820            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
17821                || (matches!(self.peek(), Token::Not)
17822                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
17823            {
17824                // v7.39 (round 711) — CARRIED now (the storing half of
17825                // F08); round 621 only consumed.
17826                let (d, idef) = self.consume_deferrable_clauses_timed()?;
17827                constraint_deferrable |= d;
17828                constraint_initially_deferred |= idef;
17829                continue;
17830            }
17831            if let Token::Ident(s) = self.peek()
17832                && s.eq_ignore_ascii_case("primary")
17833            {
17834                if is_primary_key {
17835                    return Err(self.err("PRIMARY KEY specified twice".into()));
17836                }
17837                // Peek-ahead for the required `KEY` token.
17838                let next = self.tokens.get(self.pos + 1);
17839                let next_is_key = matches!(
17840                    next,
17841                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
17842                );
17843                if !next_is_key {
17844                    return Err(self.err(format!(
17845                        "expected KEY after PRIMARY in column def, got {:?}",
17846                        next
17847                    )));
17848                }
17849                self.advance(); // PRIMARY
17850                self.advance(); // KEY
17851                is_primary_key = true;
17852                if nullability_seen && nullable {
17853                    return Err(self.err(
17854                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
17855                    ));
17856                }
17857                nullable = false;
17858                nullability_seen = true;
17859                continue;
17860            }
17861            // v7.13.0 — inline `UNIQUE` column constraint
17862            // (mailrs round-5 G2). Fold into a single-column
17863            // table-level UNIQUE at CREATE TABLE post-process time.
17864            if let Token::Ident(s) = self.peek()
17865                && s.eq_ignore_ascii_case("unique")
17866            {
17867                if is_unique {
17868                    return Err(self.err("UNIQUE specified twice".into()));
17869                }
17870                self.advance();
17871                is_unique = true;
17872                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
17873                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
17874                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
17875                    let n1 = self.tokens.get(self.pos + 1);
17876                    let n2 = self.tokens.get(self.pos + 2);
17877                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
17878                        self.advance(); // NULLS
17879                        self.advance(); // NOT
17880                        self.advance(); // DISTINCT
17881                        unique_nulls_not_distinct = true;
17882                    } else if matches!(n1, Some(Token::Distinct)) {
17883                        self.advance(); // NULLS
17884                        self.advance(); // DISTINCT
17885                    }
17886                }
17887                continue;
17888            }
17889            // v7.13.0 — inline `CHECK (<expr>)` column constraint
17890            // (mailrs round-5 G3). PG semantics: column-level
17891            // CHECK is equivalent to a table-level CHECK. Multiple
17892            // inline CHECKs on the same column AND together.
17893            if let Token::Ident(s) = self.peek()
17894                && s.eq_ignore_ascii_case("check")
17895            {
17896                self.advance();
17897                if !matches!(self.peek(), Token::LParen) {
17898                    return Err(self.err(alloc::format!(
17899                        "expected '(' after CHECK in column def, got {:?}",
17900                        self.peek()
17901                    )));
17902                }
17903                self.advance();
17904                let pred = self.parse_expr(0)?;
17905                if !matches!(self.peek(), Token::RParen) {
17906                    return Err(self.err(alloc::format!(
17907                        "expected ')' to close CHECK predicate, got {:?}",
17908                        self.peek()
17909                    )));
17910                }
17911                self.advance();
17912                check = Some(match check.take() {
17913                    Some(prev) => Expr::Binary {
17914                        op: BinOp::And,
17915                        lhs: Box::new(prev),
17916                        rhs: Box::new(pred),
17917                    },
17918                    None => pred,
17919                });
17920                continue;
17921            }
17922            break;
17923        }
17924        Ok(ColumnDef {
17925            name,
17926            ty,
17927            nullable,
17928            default,
17929            auto_increment,
17930            is_primary_key,
17931            is_unique,
17932            unique_nulls_not_distinct,
17933            constraint_deferrable,
17934            constraint_initially_deferred,
17935            check,
17936            user_type_ref,
17937            on_update_runtime,
17938            collation,
17939            collation_explicit,
17940            collation_name,
17941            is_unsigned,
17942            inline_enum_variants,
17943            inline_set_variants,
17944            generated_stored_expr,
17945            identity_always,
17946            mysql_int_width,
17947            mysql_fsp,
17948            mysql_declared_timestamp,
17949            mysql_float_md,
17950        })
17951    }
17952
17953    /// `NUMERIC` may appear without parameters, with one (precision
17954    /// only, scale=0), or with both. Returns `(precision, scale)` with
17955    /// 0 = unspecified for the bare form.
17956    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
17957        if !matches!(self.peek(), Token::LParen) {
17958            // Bare `NUMERIC` — PG treats this as "unlimited precision";
17959            // we surface it as precision=0 to mean "unconstrained" so
17960            // the engine doesn't need a separate variant.
17961            return Ok((0, 0));
17962        }
17963        self.advance();
17964        // v7.39 (round 272) — PG's declared precision runs to 1000, and
17965        // it words the out-of-range case with the value it saw. SPG
17966        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
17967        // accepts failed to parse at all; values wider than i128 are
17968        // carried by the arbitrary-precision form.
17969        let precision = match self.advance() {
17970            Token::Integer(n) if (1..=1000).contains(&n) => {
17971                u16::try_from(n).expect("range-checked")
17972            }
17973            Token::Integer(n) => {
17974                return Err(ParseError {
17975                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
17976                    token_pos: self.consumed_pos(),
17977                });
17978            }
17979            other => {
17980                return Err(ParseError {
17981                    message: format!(
17982                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
17983                    ),
17984                    token_pos: self.consumed_pos(),
17985                });
17986            }
17987        };
17988        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
17989        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
17990        // then overflows). A negative scale rounds to tens / hundreds / …
17991        let scale = if matches!(self.peek(), Token::Comma) {
17992            self.advance();
17993            let neg = if matches!(self.peek(), Token::Minus) {
17994                self.advance();
17995                true
17996            } else {
17997                false
17998            };
17999            match self.advance() {
18000                Token::Integer(n) => {
18001                    let signed = if neg { -n } else { n };
18002                    if !(-1000..=1000).contains(&signed) {
18003                        return Err(ParseError {
18004                            message: format!(
18005                                "NUMERIC scale {signed} must be between -1000 and 1000"
18006                            ),
18007                            token_pos: self.consumed_pos(),
18008                        });
18009                    }
18010                    i16::try_from(signed).expect("range-checked")
18011                }
18012                other => {
18013                    return Err(ParseError {
18014                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
18015                        token_pos: self.consumed_pos(),
18016                    });
18017                }
18018            }
18019        } else {
18020            0
18021        };
18022        if !matches!(self.peek(), Token::RParen) {
18023            return Err(self.err(format!(
18024                "expected ')' to close NUMERIC params, got {:?}",
18025                self.peek()
18026            )));
18027        }
18028        self.advance();
18029        Ok((precision, scale))
18030    }
18031
18032    /// Parse `(N)` where `N` is a positive integer literal — used by the
18033    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
18034    /// for the error message.
18035    /// v6.0.1: parse the optional `USING <encoding>` clause that
18036    /// follows `VECTOR(N)` in a column definition. Missing clause
18037    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18038    /// ident → `ParseError` listing the encodings recognised today.
18039    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18040        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18041            return Ok(VecEncoding::F32);
18042        }
18043        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18044        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18045        // consume the token when the very next token is a known
18046        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18047        // `USING` for the caller — it's the rewrite-expression form.
18048        let n1 = self.tokens.get(self.pos + 1);
18049        let next_is_encoding = matches!(
18050            n1,
18051            Some(Token::Ident(s))
18052                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18053        );
18054        if !next_is_encoding {
18055            return Ok(VecEncoding::F32);
18056        }
18057        self.advance();
18058        let enc_ident = match self.advance() {
18059            Token::Ident(s) => s,
18060            other => {
18061                return Err(self.err(format!(
18062                    "expected vector encoding after USING, got {other:?}"
18063                )));
18064            }
18065        };
18066        match enc_ident.to_ascii_lowercase().as_str() {
18067            "sq8" => Ok(VecEncoding::Sq8),
18068            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18069            // binary16 per-element storage.
18070            "half" => Ok(VecEncoding::F16),
18071            other => Err(self.err(format!(
18072                "unknown vector encoding {other:?}; supported: SQ8, HALF"
18073            ))),
18074        }
18075    }
18076
18077    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18078    /// without consuming it. Returns `Some(N)` when the next
18079    /// tokens are `( <int> )`; None otherwise. Used by the
18080    /// TINYINT classifier to decide whether to map to Bool or
18081    /// SmallInt.
18082    fn peek_optional_paren_size_value(&self) -> Option<i64> {
18083        if !matches!(self.peek(), Token::LParen) {
18084            return None;
18085        }
18086        let next = self.tokens.get(self.pos + 1)?;
18087        let n = match next {
18088            Token::Integer(n) => *n,
18089            _ => return None,
18090        };
18091        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18092            return None;
18093        }
18094        Some(n)
18095    }
18096
18097    /// v7.14.0 — consume an optional MySQL display-width
18098    /// parenthesised number after an integer type, returning
18099    /// nothing. `TINYINT(1)` etc.
18100    /// v7.39 (round 360) — does the parenthesised group ahead contain a
18101    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18102    fn peek_paren_has_comma(&self) -> bool {
18103        let mut i = self.pos + 1;
18104        let mut depth = 1usize;
18105        while depth > 0 {
18106            match self.tokens.get(i) {
18107                Some(Token::LParen) => depth += 1,
18108                Some(Token::RParen) => depth -= 1,
18109                Some(Token::Comma) if depth == 1 => return true,
18110                None | Some(Token::Eof) => return false,
18111                _ => {}
18112            }
18113            i += 1;
18114        }
18115        false
18116    }
18117
18118    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18119    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18120    /// fractional-seconds precision that drives write truncation and render
18121    /// padding, where `consume_optional_paren_size` throws it away.
18122    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18123    fn take_optional_paren_size(&mut self) -> Option<u8> {
18124        let Some(Token::Integer(n)) = self
18125            .tokens
18126            .get(self.pos + 1)
18127            .filter(|_| matches!(self.peek(), Token::LParen))
18128            .cloned()
18129        else {
18130            self.consume_optional_paren_size();
18131            return None;
18132        };
18133        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18134            self.consume_optional_paren_size();
18135            return None;
18136        }
18137        self.consume_optional_paren_size();
18138        u8::try_from(n).ok()
18139    }
18140
18141    fn consume_optional_paren_size(&mut self) {
18142        if !matches!(self.peek(), Token::LParen) {
18143            return;
18144        }
18145        self.advance();
18146        // Skip until matching RParen (allow nested or any tokens).
18147        let mut depth = 1usize;
18148        while depth > 0 {
18149            match self.peek() {
18150                Token::LParen => depth += 1,
18151                Token::RParen => depth -= 1,
18152                Token::Eof => return,
18153                _ => {}
18154            }
18155            self.advance();
18156        }
18157    }
18158
18159    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18160        if !matches!(self.peek(), Token::LParen) {
18161            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18162        }
18163        self.advance();
18164        let n = match self.advance() {
18165            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18166                message: format!("{label} size too large: {n}"),
18167                token_pos: self.consumed_pos(),
18168            })?,
18169            other => {
18170                return Err(ParseError {
18171                    message: format!("expected positive integer {label} size, got {other:?}"),
18172                    token_pos: self.consumed_pos(),
18173                });
18174            }
18175        };
18176        if !matches!(self.peek(), Token::RParen) {
18177            return Err(self.err(format!(
18178                "expected ')' after {label} size, got {:?}",
18179                self.peek()
18180            )));
18181        }
18182        self.advance();
18183        Ok(n)
18184    }
18185
18186    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18187    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18188    /// key, like MySQL) whose action skips conflicting rows.
18189    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18190    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18191    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18192    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18193    /// common bulk-upsert spellings —
18194    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18195    ///     REPLACE INTO t SELECT …
18196    /// — were a parse error / a duplicate-key failure respectively.
18197    ///
18198    /// Precedence: an explicitly written clause beats a statement-level flag.
18199    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18200    /// implicit `REPLACE` and `IGNORE` lowerings.
18201    fn parse_insert_conflict_clause(
18202        &mut self,
18203        replace: bool,
18204        ignore: bool,
18205    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18206        if let Some(c) = self.parse_optional_on_duplicate_key()? {
18207            return Ok(Some(c));
18208        }
18209        if let Some(c) = self.parse_optional_on_conflict()? {
18210            return Ok(Some(c));
18211        }
18212        if replace {
18213            // REPLACE INTO = delete-then-insert, which PG spells as
18214            // `ON CONFLICT DO UPDATE SET` over every column; the engine
18215            // reads an empty assignment list as "take the incoming row".
18216            return Ok(Some(crate::ast::OnConflictClause {
18217                target_columns: Vec::new(),
18218                index_where: None,
18219                constraint_name: None,
18220                mysql_lowered: true,
18221                action: crate::ast::OnConflictAction::Update {
18222                    assignments: Vec::new(),
18223                    where_: None,
18224                },
18225            }));
18226        }
18227        if ignore {
18228            return Ok(Some(Self::insert_ignore_clause()));
18229        }
18230        Ok(None)
18231    }
18232
18233    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18234    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18235    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18236    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18237    fn parse_optional_on_duplicate_key(
18238        &mut self,
18239    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18240        if !(matches!(self.peek(), Token::On)
18241            && matches!(self.tokens.get(self.pos + 1),
18242                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18243        {
18244            return Ok(None);
18245        }
18246        self.advance(); // ON
18247        self.advance(); // DUPLICATE
18248        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18249            return Err(self.err(format!(
18250                "expected KEY after ON DUPLICATE, got {:?}",
18251                self.peek()
18252            )));
18253        }
18254        self.advance();
18255        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18256            return Err(self.err(format!(
18257                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18258                self.peek()
18259            )));
18260        }
18261        self.advance();
18262        let mut assignments: Vec<(String, Expr)> = Vec::new();
18263        loop {
18264            let col = self.expect_ident_like()?;
18265            if !matches!(self.peek(), Token::Eq) {
18266                return Err(self.err(format!(
18267                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18268                    self.peek()
18269                )));
18270            }
18271            self.advance();
18272            let mut expr = self.parse_expr(0)?;
18273            Self::rewrite_mysql_values_refs(&mut expr);
18274            assignments.push((col, expr));
18275            if matches!(self.peek(), Token::Comma) {
18276                self.advance();
18277                continue;
18278            }
18279            break;
18280        }
18281        Ok(Some(crate::ast::OnConflictClause {
18282            target_columns: Vec::new(),
18283            index_where: None,
18284            constraint_name: None,
18285            mysql_lowered: true,
18286            action: crate::ast::OnConflictAction::Update {
18287                assignments,
18288                where_: None,
18289            },
18290        }))
18291    }
18292
18293    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18294        crate::ast::OnConflictClause {
18295            target_columns: Vec::new(),
18296            index_where: None,
18297            constraint_name: None,
18298            mysql_lowered: true,
18299            action: crate::ast::OnConflictAction::Nothing,
18300        }
18301    }
18302
18303    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18304        debug_assert!(
18305            matches!(self.peek(), Token::Insert)
18306                || (replace
18307                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18308        );
18309        self.advance();
18310        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18311        // would raise a duplicate-key error instead of failing the statement,
18312        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18313        // plain ident to the lexer; only the MySQL dialect accepts it here.
18314        let ignore = self.mysql_dialect
18315            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18316        if ignore {
18317            self.advance();
18318        }
18319        if !matches!(self.peek(), Token::Into) {
18320            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18321        }
18322        self.advance();
18323        let table = self.expect_ident_like()?;
18324        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18325        // grammar requires the AS keyword here (a bare identifier would be
18326        // ambiguous with a column list). The alias is what the ON CONFLICT
18327        // DO UPDATE expressions refer to the target row by.
18328        let alias = if matches!(self.peek(), Token::As) {
18329            self.advance();
18330            Some(self.expect_ident_like()?)
18331        } else {
18332            None
18333        };
18334        // v7.39 (round 428) — MySQL's SET-form INSERT:
18335        //     INSERT INTO t SET a = 1, b = 'x'
18336        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18337        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18338        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18339        // measured). So it lowers to the column list + one VALUES row and
18340        // rejoins the ordinary path, which already handles every one of
18341        // those. PG has no such spelling, hence the dialect gate.
18342        if self.mysql_dialect
18343            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18344        {
18345            self.advance(); // SET
18346            let mut names = Vec::new();
18347            let mut values = Vec::new();
18348            loop {
18349                names.push(self.expect_ident_like()?);
18350                if !matches!(self.peek(), Token::Eq) {
18351                    return Err(self.err(alloc::format!(
18352                        "expected '=' in INSERT … SET, got {:?}",
18353                        self.peek()
18354                    )));
18355                }
18356                self.advance();
18357                // `SET a = DEFAULT` rides the same `__column_default` marker
18358                // the VALUES-row and UPDATE-SET paths use; the INSERT
18359                // executor resolves it against the target column.
18360                if matches!(self.peek(), Token::Default) {
18361                    self.advance();
18362                    values.push(Expr::FunctionCall {
18363                        name: "__column_default".to_string(),
18364                        args: Vec::new(),
18365                    });
18366                } else {
18367                    values.push(self.parse_expr(0)?);
18368                }
18369                if matches!(self.peek(), Token::Comma) {
18370                    self.advance();
18371                    continue;
18372                }
18373                break;
18374            }
18375            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18376            let returning = self.parse_optional_returning()?;
18377            return Ok(Statement::Insert(InsertStatement {
18378                ctes: Vec::new(),
18379                table,
18380                alias,
18381                columns: Some(names),
18382                rows: alloc::vec![values],
18383                select_source: None,
18384                // MySQL's SET form has no `OVERRIDING …` clause (that is
18385                // PG's identity-column spelling).
18386                overriding: Overriding::None,
18387                mysql_ignore: ignore,
18388                on_conflict,
18389                returning,
18390            }));
18391        }
18392        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18393        // v7.39 (round 151) — a SELECT or WITH right after the paren is
18394        // a parenthesized query source instead (PG select_with_parens:
18395        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18396        // both keywords are reserved in PG, so no column list can start
18397        // with them.
18398        let columns = if matches!(self.peek(), Token::LParen) {
18399            self.advance();
18400            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18401                let select_stmt = if self.peek_is_with_kw() {
18402                    self.advance();
18403                    self.parse_nested_with_select()?
18404                } else {
18405                    match self.parse_select_stmt()? {
18406                        Statement::Select(s) => s,
18407                        other => {
18408                            return Err(self.err(alloc::format!(
18409                                "expected SELECT in parenthesized INSERT source, got {other:?}"
18410                            )));
18411                        }
18412                    }
18413                };
18414                if !matches!(self.peek(), Token::RParen) {
18415                    return Err(self.err(format!(
18416                        "expected ')' after parenthesized INSERT source, got {:?}",
18417                        self.peek()
18418                    )));
18419                }
18420                self.advance();
18421                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18422                let returning = self.parse_optional_returning()?;
18423                return Ok(Statement::Insert(InsertStatement {
18424                    ctes: Vec::new(),
18425                    table,
18426                    alias: alias.clone(),
18427                    columns: None,
18428                    rows: Vec::new(),
18429                    select_source: Some(Box::new(select_stmt)),
18430                    on_conflict,
18431                    returning,
18432                    overriding: Overriding::None,
18433                    mysql_ignore: ignore,
18434                }));
18435            }
18436            let mut names = Vec::new();
18437            loop {
18438                names.push(self.expect_ident_like()?);
18439                match self.peek() {
18440                    Token::Comma => {
18441                        self.advance();
18442                    }
18443                    Token::RParen => {
18444                        self.advance();
18445                        break;
18446                    }
18447                    other => {
18448                        return Err(self.err(format!(
18449                            "expected ',' or ')' in INSERT column list, got {other:?}"
18450                        )));
18451                    }
18452                }
18453            }
18454            Some(names)
18455        } else {
18456            None
18457        };
18458        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18459        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18460        // is captured on the statement so the engine can apply PG's
18461        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18462        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18463        {
18464            self.advance();
18465            let which = self.expect_ident_like()?;
18466            let ov = if which.eq_ignore_ascii_case("system") {
18467                Overriding::System
18468            } else if which.eq_ignore_ascii_case("user") {
18469                Overriding::User
18470            } else {
18471                return Err(self.err(format!(
18472                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18473                )));
18474            };
18475            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18476                return Err(self.err(format!(
18477                    "expected VALUE after OVERRIDING {}, got {:?}",
18478                    which.to_ascii_uppercase(),
18479                    self.peek()
18480                )));
18481            }
18482            self.advance();
18483            ov
18484        } else {
18485            Overriding::None
18486        };
18487        // `INSERT INTO t DEFAULT VALUES` — a single row made
18488        // entirely of column defaults. Lower to the permuted
18489        // column-list path with an empty list: every schema column
18490        // is unmapped, so the engine fills each from its default
18491        // (serials advance, plain defaults evaluate, the rest NULL).
18492        if matches!(self.peek(), Token::Default) {
18493            self.advance();
18494            if !matches!(self.peek(), Token::Values) {
18495                return Err(self.err(format!(
18496                    "expected VALUES after DEFAULT in INSERT, got {:?}",
18497                    self.peek()
18498                )));
18499            }
18500            self.advance();
18501            if columns.is_some() {
18502                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18503            }
18504            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18505            let returning = self.parse_optional_returning()?;
18506            return Ok(Statement::Insert(InsertStatement {
18507                ctes: Vec::new(),
18508                table,
18509                alias: alias.clone(),
18510                columns: Some(Vec::new()),
18511                rows: alloc::vec![Vec::new()],
18512                select_source: None,
18513                on_conflict,
18514                returning,
18515                overriding,
18516                mysql_ignore: ignore,
18517            }));
18518        }
18519        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18520        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18521        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18522        // SELECT …`) heads the SOURCE select, as in PG (the statement's
18523        // own WITH comes before INSERT).
18524        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18525            let select_stmt = if self.peek_is_with_kw() {
18526                self.advance();
18527                self.parse_nested_with_select()?
18528            } else {
18529                match self.parse_select_stmt()? {
18530                    Statement::Select(s) => s,
18531                    other => {
18532                        return Err(self.err(alloc::format!(
18533                            "expected SELECT after INSERT INTO ... target, got {other:?}"
18534                        )));
18535                    }
18536                }
18537            };
18538            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18539            let returning = self.parse_optional_returning()?;
18540            return Ok(Statement::Insert(InsertStatement {
18541                ctes: Vec::new(),
18542                table,
18543                alias: alias.clone(),
18544                columns,
18545                rows: Vec::new(),
18546                select_source: Some(Box::new(select_stmt)),
18547                on_conflict,
18548                returning,
18549                overriding,
18550                mysql_ignore: ignore,
18551            }));
18552        }
18553        if !matches!(self.peek(), Token::Values) {
18554            return Err(self.err(format!(
18555                "expected VALUES or SELECT after table name, got {:?}",
18556                self.peek()
18557            )));
18558        }
18559        self.advance();
18560        if !matches!(self.peek(), Token::LParen) {
18561            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18562        }
18563        let mut rows = Vec::new();
18564        loop {
18565            // Each iteration consumes one `(expr, expr, …)` tuple.
18566            if !matches!(self.peek(), Token::LParen) {
18567                return Err(self.err(format!(
18568                    "expected '(' for next VALUES tuple, got {:?}",
18569                    self.peek()
18570                )));
18571            }
18572            self.advance();
18573            let mut tuple = Vec::new();
18574            loop {
18575                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18576                // the column's declared default for that slot. Rides out as the
18577                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18578                // path uses; the INSERT executor resolves it per target column.
18579                if matches!(self.peek(), Token::Default) {
18580                    self.advance();
18581                    tuple.push(Expr::FunctionCall {
18582                        name: "__column_default".to_string(),
18583                        args: Vec::new(),
18584                    });
18585                } else {
18586                    tuple.push(self.parse_expr(0)?);
18587                }
18588                match self.peek() {
18589                    Token::Comma => {
18590                        self.advance();
18591                    }
18592                    Token::RParen => {
18593                        self.advance();
18594                        break;
18595                    }
18596                    other => {
18597                        return Err(self.err(format!(
18598                            "expected ',' or ')' in VALUES tuple, got {other:?}"
18599                        )));
18600                    }
18601                }
18602            }
18603            if tuple.is_empty() {
18604                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18605            }
18606            rows.push(tuple);
18607            // Continue with comma-separated tuples.
18608            if matches!(self.peek(), Token::Comma) {
18609                self.advance();
18610            } else {
18611                break;
18612            }
18613        }
18614        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18615        // to ON CONFLICT DO UPDATE with an empty conflict target
18616        // (the engine picks the table's first unique index, which
18617        // matches MySQL's any-unique-key behaviour for the common
18618        // single-key case). `VALUES(col)` in the assignments is
18619        // MySQL's spelling of EXCLUDED.col.
18620        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18621        let returning = self.parse_optional_returning()?;
18622        Ok(Statement::Insert(InsertStatement {
18623            ctes: Vec::new(),
18624            table,
18625            alias,
18626            columns,
18627            rows,
18628            select_source: None,
18629            on_conflict,
18630            returning,
18631            overriding,
18632            mysql_ignore: ignore,
18633        }))
18634    }
18635
18636    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18637    /// the incoming row's value — exactly PG's EXCLUDED.col.
18638    fn rewrite_mysql_values_refs(e: &mut Expr) {
18639        match e {
18640            Expr::FunctionCall { name, args }
18641                if name.eq_ignore_ascii_case("values")
18642                    && args.len() == 1
18643                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
18644            {
18645                let Expr::Column(c) = &args[0] else {
18646                    unreachable!("guarded above");
18647                };
18648                *e = Expr::Column(crate::ast::ColumnName {
18649                    qualifier: Some("EXCLUDED".to_string()),
18650                    name: c.name.clone(),
18651                });
18652            }
18653            Expr::FunctionCall { args, .. } => {
18654                for a in args {
18655                    Self::rewrite_mysql_values_refs(a);
18656                }
18657            }
18658            Expr::Binary { lhs, rhs, .. } => {
18659                Self::rewrite_mysql_values_refs(lhs);
18660                Self::rewrite_mysql_values_refs(rhs);
18661            }
18662            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
18663                Self::rewrite_mysql_values_refs(expr);
18664            }
18665            Expr::Case {
18666                operand,
18667                branches,
18668                else_branch,
18669            } => {
18670                if let Some(op) = operand {
18671                    Self::rewrite_mysql_values_refs(op);
18672                }
18673                for (w, t) in branches {
18674                    Self::rewrite_mysql_values_refs(w);
18675                    Self::rewrite_mysql_values_refs(t);
18676                }
18677                if let Some(el) = else_branch {
18678                    Self::rewrite_mysql_values_refs(el);
18679                }
18680            }
18681            _ => {}
18682        }
18683    }
18684
18685    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
18686    /// clause sitting between the INSERT body and the trailing
18687    /// RETURNING. All keywords come in as bare idents; `ON` is
18688    /// a reserved Token though.
18689    fn parse_optional_on_conflict(
18690        &mut self,
18691    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18692        if !matches!(self.peek(), Token::On) {
18693            return Ok(None);
18694        }
18695        // Peek further: we want exactly "ON CONFLICT ...". If the
18696        // next ident isn't "conflict", let some other parser handle.
18697        let next_is_conflict = matches!(
18698            self.tokens.get(self.pos + 1),
18699            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
18700        );
18701        if !next_is_conflict {
18702            return Ok(None);
18703        }
18704        self.advance(); // ON
18705        self.advance(); // CONFLICT
18706        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
18707        // the constraint instead of listing columns (the pg_dump
18708        // form); the engine resolves it.
18709        let mut constraint_name: Option<String> = None;
18710        if matches!(self.peek(), Token::On) {
18711            self.advance(); // ON
18712            match self.advance() {
18713                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
18714                }
18715                other => {
18716                    return Err(self.err(alloc::format!(
18717                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
18718                    )));
18719                }
18720            }
18721            constraint_name = Some(self.expect_ident_like()?);
18722        }
18723        // Optional `(col [, col]*)` target list.
18724        let mut target_columns: Vec<String> = Vec::new();
18725        if matches!(self.peek(), Token::LParen) {
18726            self.advance();
18727            loop {
18728                target_columns.push(self.expect_ident_like()?);
18729                match self.peek() {
18730                    Token::Comma => {
18731                        self.advance();
18732                    }
18733                    Token::RParen => {
18734                        self.advance();
18735                        break;
18736                    }
18737                    other => {
18738                        return Err(self.err(alloc::format!(
18739                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
18740                        )));
18741                    }
18742                }
18743            }
18744        }
18745        // v7.39 (round 240) — optional index predicate after the target
18746        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
18747        // PARTIAL unique index; SPG's arbiters are full indexes, which
18748        // satisfy any predicate, so it is parsed and carried but not
18749        // consulted (recorded residual: partial-unique-index arbiters).
18750        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
18751            self.advance();
18752            Some(self.parse_expr(0)?)
18753        } else {
18754            None
18755        };
18756        // Required `DO`.
18757        match self.advance() {
18758            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
18759            other => {
18760                return Err(self.err(alloc::format!(
18761                    "expected DO after ON CONFLICT [(…)], got {other:?}"
18762                )));
18763            }
18764        }
18765        // Action: NOTHING | UPDATE SET …
18766        let action = match self.advance() {
18767            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
18768                crate::ast::OnConflictAction::Nothing
18769            }
18770            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
18771                self.parse_on_conflict_update_action()?
18772            }
18773            other => {
18774                return Err(self.err(alloc::format!(
18775                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
18776                )));
18777            }
18778        };
18779        Ok(Some(crate::ast::OnConflictClause {
18780            target_columns,
18781            index_where,
18782            constraint_name,
18783            mysql_lowered: false,
18784            action,
18785        }))
18786    }
18787
18788    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
18789    /// `SET col = expr [, …] [WHERE cond]`. Caller already
18790    /// consumed `UPDATE`.
18791    fn parse_on_conflict_update_action(
18792        &mut self,
18793    ) -> Result<crate::ast::OnConflictAction, ParseError> {
18794        // `SET`
18795        match self.advance() {
18796            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
18797            other => {
18798                return Err(self.err(alloc::format!(
18799                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
18800                )));
18801            }
18802        }
18803        let mut assignments: Vec<(String, Expr)> = Vec::new();
18804        loop {
18805            let col = self.expect_ident_like()?;
18806            if !matches!(self.peek(), Token::Eq) {
18807                return Err(self.err(alloc::format!(
18808                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
18809                    self.peek()
18810                )));
18811            }
18812            self.advance();
18813            let value = self.parse_expr(0)?;
18814            assignments.push((col, value));
18815            if matches!(self.peek(), Token::Comma) {
18816                self.advance();
18817                continue;
18818            }
18819            break;
18820        }
18821        let where_ = if matches!(self.peek(), Token::Where) {
18822            self.advance();
18823            Some(self.parse_expr(0)?)
18824        } else {
18825            None
18826        };
18827        Ok(crate::ast::OnConflictAction::Update {
18828            assignments,
18829            where_,
18830        })
18831    }
18832
18833    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
18834        let mut items = Vec::new();
18835        // v7.39 (round 341, V66) — PG's target list may be EMPTY
18836        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
18837        // answers one zero-column row per row of t, and a bare `SELECT`
18838        // answers a single zero-column row. SPG required at least one
18839        // item, so both were syntax errors. Recognised by the token that
18840        // follows — nothing that can start an expression appears here.
18841        if self.select_list_is_empty_here() {
18842            return Ok(items);
18843        }
18844        loop {
18845            items.push(self.parse_select_item()?);
18846            if matches!(self.peek(), Token::Comma) {
18847                self.advance();
18848            } else {
18849                break;
18850            }
18851        }
18852        Ok(items)
18853    }
18854
18855    /// Is the target list empty at this point — i.e. does the next token
18856    /// end the SELECT's item list rather than start an item?
18857    fn select_list_is_empty_here(&self) -> bool {
18858        match self.peek() {
18859            Token::From
18860            | Token::Where
18861            | Token::Group
18862            | Token::Having
18863            | Token::Order
18864            | Token::Limit
18865            | Token::Offset
18866            | Token::Semicolon
18867            | Token::RParen
18868            | Token::Union
18869            | Token::Except
18870            | Token::Eof => true,
18871            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
18872            // with unreserved keywords, so they arrive as plain idents.
18873            Token::Ident(s) => {
18874                s.eq_ignore_ascii_case("fetch")
18875                    || s.eq_ignore_ascii_case("window")
18876                    || s.eq_ignore_ascii_case("intersect")
18877            }
18878            _ => false,
18879        }
18880    }
18881
18882    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
18883        if matches!(self.peek(), Token::Star) {
18884            self.advance();
18885            return Ok(SelectItem::Wildcard);
18886        }
18887        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
18888        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
18889        // choke on the `*` ("expected identifier, got Star"). The lookahead is
18890        // `<ident> . *` with nothing binding tighter.
18891        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
18892            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
18893                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
18894            {
18895                self.advance(); // qualifier
18896                self.advance(); // .
18897                self.advance(); // *
18898                return Ok(SelectItem::QualifiedWildcard(q));
18899            }
18900        }
18901        let start_tok = self.pos;
18902        let expr = self.parse_expr(0)?;
18903        let end_tok = self.consumed_pos();
18904        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
18905        // multi-column function returns into columns. Marked here and lowered in
18906        // `parse_bare_select`, where the FROM clause is in hand.
18907        if matches!(self.peek(), Token::Dot)
18908            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
18909        {
18910            self.advance(); // .
18911            self.advance(); // *
18912            return Ok(SelectItem::Expr {
18913                expr: Expr::FunctionCall {
18914                    name: "__record_expand".to_string(),
18915                    args: alloc::vec![expr],
18916                },
18917                alias: None,
18918            });
18919        }
18920        // v7.39.2 — MySQL lets a STRING name a projection item, with or
18921        // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
18922        // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
18923        // `syntax error at or near "'x'"` to all of them.
18924        //
18925        // Only here, not in `parse_optional_alias`: that one also names
18926        // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
18927        // and `FROM t AS 'ta'` are both syntax errors, measured. And only
18928        // after the lexer's own rule has joined adjacent literals, or
18929        // `SELECT 'a' 'b'` would read as a literal aliased `b` where
18930        // MySQL answers the concatenation `ab`.
18931        if self.mysql_dialect {
18932            let at_as = matches!(self.peek(), Token::As)
18933                && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
18934            if at_as {
18935                self.advance();
18936            }
18937            if let Token::String(name) = self.peek().clone() {
18938                self.advance();
18939                return Ok(SelectItem::Expr {
18940                    expr,
18941                    alias: Some(name),
18942                });
18943            }
18944        }
18945        let alias = match self.parse_optional_alias()? {
18946            Some(a) => Some(a),
18947            None => self.mysql_item_label(&expr, start_tok, end_tok),
18948        };
18949        Ok(SelectItem::Expr { expr, alias })
18950    }
18951
18952    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
18953    /// carries no `AS`, filled in here so every downstream path reports it
18954    /// without knowing the rule. `None` leaves the item un-aliased, which is
18955    /// what a PG session always gets.
18956    ///
18957    /// Measured against MariaDB 11, three rules and no more:
18958    ///
18959    /// | item             | label      | why                          |
18960    /// |------------------|------------|------------------------------|
18961    /// | `lbl.a`          | `a`        | a column reports its name    |
18962    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
18963    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
18964    ///
18965    /// The third is why this lives in the parser at all: the label is the
18966    /// text the client WROTE, down to the spacing, so it cannot be printed
18967    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
18968    ///
18969    /// Comments survive, and that is right: through a `mariadb` CLI both
18970    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
18971    /// CLIENT stripping the comment before it sends. Asked over the raw
18972    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
18973    /// produces.
18974    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
18975        if !self.mysql_dialect {
18976            return None;
18977        }
18978        match expr {
18979            // A column already reports its own name downstream; naming it
18980            // again here would only re-state the qualifier the label drops.
18981            Expr::Column(_) => None,
18982            // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
18983            // `ab`, and MySQL 9.7.2 names the column `a`: the label is
18984            // the first segment as written, not the joined value
18985            // (measured). The lexer logs where it joined them.
18986            Expr::Literal(Literal::String(v)) => Some(
18987                self.merged_first_len(start_tok)
18988                    .and_then(|n| v.get(..n))
18989                    .map_or_else(|| v.clone(), String::from),
18990            ),
18991            _ => self.source_span(start_tok, end_tok).map(str::to_string),
18992        }
18993    }
18994
18995    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
18996    /// consumed VALUES keyword. Each row lowers to a constant SELECT
18997    /// with PG's default column1..columnN names; subsequent rows
18998    /// chain as UNION ALL peers. Shared by the FROM-position
18999    /// `( VALUES … )` arm and the top-level bare VALUES statement.
19000    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
19001        let mut row_selects: Vec<SelectStatement> = Vec::new();
19002        loop {
19003            if !matches!(self.peek(), Token::LParen) {
19004                return Err(self.err(alloc::format!(
19005                    "expected '(' to start a VALUES row, got {:?}",
19006                    self.peek()
19007                )));
19008            }
19009            self.advance(); // (
19010            let mut items: Vec<SelectItem> = Vec::new();
19011            loop {
19012                let expr = self.parse_expr(0)?;
19013                items.push(SelectItem::Expr {
19014                    expr,
19015                    alias: Some(alloc::format!("column{}", items.len() + 1)),
19016                });
19017                match self.peek() {
19018                    Token::Comma => {
19019                        self.advance();
19020                    }
19021                    Token::RParen => break,
19022                    other => {
19023                        return Err(self.err(alloc::format!(
19024                            "expected ',' or ')' in VALUES row, got {other:?}"
19025                        )));
19026                    }
19027                }
19028            }
19029            self.advance(); // )
19030            row_selects.push(SelectStatement {
19031                locking: None,
19032                ctes: Vec::new(),
19033                distinct: false,
19034                distinct_on: Vec::new(),
19035                items,
19036                from: None,
19037                where_: None,
19038                group_by: None,
19039                group_by_all: false,
19040                having: None,
19041                unions: Vec::new(),
19042                order_by: Vec::new(),
19043                limit: None,
19044                offset: None,
19045                limit_with_ties: false,
19046                window_check_exprs: Vec::new(),
19047            });
19048            if matches!(self.peek(), Token::Comma) {
19049                self.advance();
19050                continue;
19051            }
19052            break;
19053        }
19054        let mut head = row_selects.remove(0);
19055        head.unions = row_selects
19056            .into_iter()
19057            .map(|s| (UnionKind::All, s))
19058            .collect();
19059        Ok(head)
19060    }
19061
19062    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19063        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19064        // children. It was read as a table NAMED `only`, so the query
19065        // failed on `relation "only" does not exist`.
19066        //
19067        // v7.39 (round 644) — and it is no longer a no-op. Round 621
19068        // absorbed the keyword, reasoning that SPG's children are
19069        // separate relations a plain scan does not descend into, so ONLY
19070        // already described the scan. That stopped being true when a
19071        // partition parent started unioning its children: measured,
19072        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19073        // where PG answers 0. The flag is carried now.
19074        let mut only = false;
19075        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19076            && matches!(
19077                self.tokens.get(self.pos + 1),
19078                Some(Token::Ident(_) | Token::QuotedIdent(_))
19079            )
19080        {
19081            only = true;
19082            self.advance();
19083        }
19084        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19085        // for these SRFs the keyword is noise at parse time: the
19086        // join executor already substitutes outer-column references
19087        // into unnest_expr / generate_series_args per outer row
19088        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19089        // licences the correlation even without the keyword. Absorb
19090        // it and fall through to the SRF arms below.
19091        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19092        // just the four builtin SRFs: a user set-returning function on a JOIN's
19093        // right side is the whole point of LATERAL. The keyword stays noise at
19094        // parse time — the join executor substitutes the outer row into the
19095        // call's arguments per outer row.
19096        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19097            && matches!(
19098                self.tokens.get(self.pos + 1),
19099                // The json_each family has its OWN `LATERAL …` arm below, which
19100                // needs to see the keyword — absorbing it here would send those
19101                // calls down the generic table-function channel instead.
19102                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19103            )
19104            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19105        {
19106            self.advance(); // LATERAL
19107        }
19108        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19109        // set-returning function whose argument may reference a
19110        // preceding FROM item. We rewrite this to
19111        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19112        // AS __srf__) AS <alias>` so the existing LATERAL subquery
19113        // executor handles per-outer-row evaluation and the
19114        // SRF-primary jsonb_each_text path handles the inner
19115        // materialisation. Sentori 0067 backfill is the dogfood
19116        // shape.
19117        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19118            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19119            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19120        {
19121            self.advance(); // LATERAL
19122            let each_fn = match self.peek() {
19123                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19124                _ => unreachable!(),
19125            };
19126            self.advance(); // jsonb_each[_text] / json_each[_text]
19127            self.advance(); // (
19128            let arg = self.parse_expr(0)?;
19129            if !matches!(self.peek(), Token::RParen) {
19130                return Err(self.err(alloc::format!(
19131                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19132                    self.peek()
19133                )));
19134            }
19135            self.advance();
19136            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19137            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19138            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19139            //               FROM jsonb_each_text(<arg>) AS __srf__
19140            // PG's `AS kv(key, value)` column-alias list maps
19141            // positions to names; default to (key, value) when
19142            // omitted (matching the SRF's natural column names).
19143            let srf_alias = "__srf__".to_string();
19144            let key_alias = column_aliases
19145                .first()
19146                .cloned()
19147                .unwrap_or_else(|| "key".to_string());
19148            let value_alias = column_aliases
19149                .get(1)
19150                .cloned()
19151                .unwrap_or_else(|| "value".to_string());
19152            let inner_select = crate::ast::SelectStatement {
19153                locking: None,
19154                ctes: Vec::new(),
19155                distinct: false,
19156                distinct_on: Vec::new(),
19157                items: alloc::vec![
19158                    crate::ast::SelectItem::Expr {
19159                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19160                            qualifier: Some(srf_alias.clone()),
19161                            name: "key".to_string(),
19162                        }),
19163                        alias: Some(key_alias),
19164                    },
19165                    crate::ast::SelectItem::Expr {
19166                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19167                            qualifier: Some(srf_alias.clone()),
19168                            name: "value".to_string(),
19169                        }),
19170                        alias: Some(value_alias),
19171                    },
19172                ],
19173                from: Some(crate::ast::FromClause {
19174                    primary: TableRef {
19175                        name: srf_alias.clone(),
19176                        alias: Some(srf_alias.clone()),
19177                        only: false,
19178                        as_of_segment: None,
19179                        unnest_expr: None,
19180                        unnest_column_aliases: Vec::new(),
19181                        with_ordinality: false,
19182                        generate_series_args: None,
19183                        lateral_subquery: None,
19184                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19185                        table_fn_call: None,
19186                        rows_from: None,
19187                        json_table: None,
19188                        scalar_fn_item: false,
19189                    },
19190                    joins: Vec::new(),
19191                }),
19192                where_: None,
19193                group_by: None,
19194                group_by_all: false,
19195                having: None,
19196                unions: Vec::new(),
19197                order_by: Vec::new(),
19198                limit: None,
19199                offset: None,
19200                limit_with_ties: false,
19201                window_check_exprs: Vec::new(),
19202            };
19203            return Ok(TableRef {
19204                name: alias.clone(),
19205                alias: Some(alias),
19206                only: false,
19207                as_of_segment: None,
19208                unnest_expr: None,
19209                unnest_column_aliases: Vec::new(),
19210                with_ordinality: false,
19211                generate_series_args: None,
19212                lateral_subquery: Some(Box::new(inner_select)),
19213                jsonb_each_text_arg: None,
19214                table_fn_call: None,
19215                rows_from: None,
19216                json_table: None,
19217                scalar_fn_item: false,
19218            });
19219        }
19220        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19221        // without an explicit `LATERAL` keyword is the same shape
19222        // PG accepts (SRF naturally licences lateral correlation).
19223        // We mirror the LATERAL rewrite when the argument syntactic-
19224        // ally references an outer column (Column { qualifier:
19225        // Some(_), … }). For simplicity we apply the rewrite
19226        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19227        // in the FROM-list — caller-side join parsing positions
19228        // this peek correctly.
19229        // (Implementation note: detection lives below; the LATERAL
19230        // branch above already covers the explicit form; the bare
19231        // form falls through to the plain SRF arm and the engine
19232        // treats it as a constant-arg SRF if no outer reference is
19233        // present.)
19234        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19235        // table. Detect at the head so it claims precedence over
19236        // every other table-ref shape (unnest / generate_series /
19237        // bare ident); the lateral subquery itself follows the
19238        // regular SELECT grammar.
19239        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19240        // t(cols)`. Each row lowers to a constant SELECT with PG's
19241        // default column1..columnN names; subsequent rows chain as
19242        // UNION ALL peers. The result rides the derived-table
19243        // lateral_subquery channel — zero executor work.
19244        if matches!(self.peek(), Token::LParen)
19245            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19246        {
19247            self.advance(); // (
19248            self.advance(); // VALUES
19249            let head = self.parse_values_rows_body()?;
19250            if !matches!(self.peek(), Token::RParen) {
19251                return Err(self.err(alloc::format!(
19252                    "expected ')' after VALUES list, got {:?}",
19253                    self.peek()
19254                )));
19255            }
19256            self.advance();
19257            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19258            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19259            return Ok(TableRef {
19260                name,
19261                alias: alias_ident,
19262                only: false,
19263                as_of_segment: None,
19264                unnest_expr: None,
19265                unnest_column_aliases: column_aliases,
19266                with_ordinality: false,
19267                generate_series_args: None,
19268                lateral_subquery: Some(Box::new(head)),
19269                jsonb_each_text_arg: None,
19270                table_fn_call: None,
19271                rows_from: None,
19272                json_table: None,
19273                scalar_fn_item: false,
19274            });
19275        }
19276        // v7.37.17 (17.6 siblings) — plain derived table:
19277        // `FROM ( SELECT … ) [AS] alias`. Rides the same
19278        // lateral_subquery channel the explicit LATERAL form uses —
19279        // an uncorrelated inner SELECT executes identically. The
19280        // inner parse carries UNION tails (they live on
19281        // SelectStatement.unions).
19282        // v7.37 D.20 — the derived-table inner may itself be a
19283        // parenthesized set-operation group (`FROM ((SELECT…) UNION
19284        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19285        // bare `(SELECT …)`. parse_one_statement already routes a leading
19286        // `(` set-op group (its LParen arm) and a leading WITH
19287        // (parse_with_cte_then_select), so widen the second-token gate to
19288        // Select | LParen | WITH.
19289        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19290        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19291        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19292        // has existed since the shorthand landed and `parse_bare_select`
19293        // already routes it ("valid anywhere a SELECT head is"); what was
19294        // missing is this second-token gate, and the CTE body's dispatch
19295        // below. Round 868 found both by putting the shorthand in a
19296        // subquery — the top-level forms had been the only ones tested.
19297        if matches!(self.peek(), Token::LParen)
19298            && (matches!(
19299                self.tokens.get(self.pos + 1),
19300                Some(Token::Select | Token::LParen | Token::Table)
19301            ) || matches!(self.tokens.get(self.pos + 1),
19302                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19303        {
19304            self.advance(); // (
19305            let inner = match self.parse_one_statement()? {
19306                Statement::Select(s) => s,
19307                other => {
19308                    return Err(self.err(alloc::format!(
19309                        "expected SELECT inside derived table ( … ), got {other:?}"
19310                    )));
19311                }
19312            };
19313            if !matches!(self.peek(), Token::RParen) {
19314                return Err(self.err(alloc::format!(
19315                    "expected ')' after derived-table subquery, got {:?}",
19316                    self.peek()
19317                )));
19318            }
19319            self.advance();
19320            // `AS t(a, b)` column-alias list rides the
19321            // unnest_column_aliases field (same positional-rename
19322            // contract the unnest SRFs use).
19323            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19324            let name = alias_ident
19325                .clone()
19326                .unwrap_or_else(|| "subquery".to_string());
19327            return Ok(TableRef {
19328                name,
19329                alias: alias_ident,
19330                only: false,
19331                as_of_segment: None,
19332                unnest_expr: None,
19333                unnest_column_aliases: column_aliases,
19334                with_ordinality: false,
19335                generate_series_args: None,
19336                lateral_subquery: Some(Box::new(inner)),
19337                jsonb_each_text_arg: None,
19338                table_fn_call: None,
19339                rows_from: None,
19340                json_table: None,
19341                scalar_fn_item: false,
19342            });
19343        }
19344        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19345            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19346        {
19347            self.advance(); // LATERAL
19348            self.advance(); // (
19349            // Parse the inner SELECT.
19350            let inner = match self.parse_one_statement()? {
19351                Statement::Select(s) => s,
19352                other => {
19353                    return Err(self.err(alloc::format!(
19354                        "expected SELECT inside LATERAL ( … ), got {other:?}"
19355                    )));
19356                }
19357            };
19358            if !matches!(self.peek(), Token::RParen) {
19359                return Err(self.err(alloc::format!(
19360                    "expected ')' after LATERAL subquery, got {:?}",
19361                    self.peek()
19362                )));
19363            }
19364            self.advance();
19365            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19366            // `(VALUES …) t(g)` derived table round-trips through view-body
19367            // Display, which renders on the lateral_subquery channel).
19368            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19369            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19370            return Ok(TableRef {
19371                name,
19372                alias: alias_ident,
19373                only: false,
19374                as_of_segment: None,
19375                unnest_expr: None,
19376                unnest_column_aliases: column_aliases,
19377                with_ordinality: false,
19378                generate_series_args: None,
19379                lateral_subquery: Some(Box::new(inner)),
19380                jsonb_each_text_arg: None,
19381                table_fn_call: None,
19382                rows_from: None,
19383                json_table: None,
19384                scalar_fn_item: false,
19385            });
19386        }
19387        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19388        // function as a FROM item. Emits one row per (key, value)
19389        // pair in the JSONB object argument as TEXT columns. May
19390        // be wrapped in CROSS JOIN LATERAL when the argument
19391        // references a preceding FROM item (sentori migration
19392        // 0067 backfill shape: `CROSS JOIN LATERAL
19393        // jsonb_each_text(t.json_col) AS kv(key, value)`).
19394        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19395            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19396        {
19397            let each_fn = match self.peek() {
19398                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19399                _ => unreachable!(),
19400            };
19401            self.advance(); // jsonb_each[_text] / json_each[_text]
19402            self.advance(); // (
19403            let arg = self.parse_expr(0)?;
19404            if !matches!(self.peek(), Token::RParen) {
19405                return Err(self.err(alloc::format!(
19406                    "expected ')' after {each_fn}() argument, got {:?}",
19407                    self.peek()
19408                )));
19409            }
19410            self.advance();
19411            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19412            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19413            return Ok(TableRef {
19414                name,
19415                alias: alias_ident,
19416                only: false,
19417                as_of_segment: None,
19418                unnest_expr: None,
19419                // `AS t(k, v)` renames key/value positionally, same as the
19420                // LATERAL-position form already does.
19421                unnest_column_aliases: column_aliases,
19422                with_ordinality: false,
19423                generate_series_args: None,
19424                lateral_subquery: None,
19425                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19426                table_fn_call: None,
19427                rows_from: None,
19428                json_table: None,
19429                scalar_fn_item: false,
19430            });
19431        }
19432        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19433        // (+ json_ variants) — record-returning JSON functions with a
19434        // column-definition list. Desugar to a derived table that
19435        // projects each declared column from the JSON via `->>` + a cast,
19436        // over `jsonb_array_elements(J)` for the *set (per-element) form.
19437        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19438            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19439        {
19440            return self.parse_json_to_record_from();
19441        }
19442        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19443        // row is a text[] of capture groups, so it cannot desugar to unnest
19444        // (that would flatten the array). Wrap it as a derived table
19445        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19446        // SRF path already emits one text[] row per match. PG names the column
19447        // `regexp_matches`; an `AS a(col)` alias overrides it.
19448        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19449                if s.eq_ignore_ascii_case("regexp_matches"))
19450            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19451        {
19452            self.advance(); // fn name
19453            self.advance(); // (
19454            let mut fn_args: Vec<Expr> = Vec::new();
19455            loop {
19456                fn_args.push(self.parse_expr(0)?);
19457                if matches!(self.peek(), Token::Comma) {
19458                    self.advance();
19459                    continue;
19460                }
19461                break;
19462            }
19463            if !matches!(self.peek(), Token::RParen) {
19464                return Err(self.err(alloc::format!(
19465                    "expected ')' after regexp_matches() arguments, got {:?}",
19466                    self.peek()
19467                )));
19468            }
19469            self.advance();
19470            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19471            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19472            // it, so it died on the `with` token while every other table function
19473            // accepted it.
19474            let with_ordinality = self.absorb_with_ordinality();
19475            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19476            let table_alias = alias_ident
19477                .clone()
19478                .unwrap_or_else(|| "regexp_matches".to_string());
19479            // PG names a single-column function's output column after the ALIAS
19480            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19481            // `m` reads as that column and not as a whole-row composite. Naming
19482            // it after the function regardless made `SELECT m[1] FROM … AS m`
19483            // subscript a record.
19484            let col_name = column_aliases
19485                .first()
19486                .cloned()
19487                .or_else(|| alias_ident.clone())
19488                .unwrap_or_else(|| "regexp_matches".to_string());
19489            let inner = crate::ast::SelectStatement {
19490                locking: None,
19491                ctes: Vec::new(),
19492                distinct: false,
19493                distinct_on: Vec::new(),
19494                items: alloc::vec![SelectItem::Expr {
19495                    expr: Expr::FunctionCall {
19496                        name: "regexp_matches".to_string(),
19497                        args: fn_args,
19498                    },
19499                    alias: Some(col_name),
19500                }],
19501                from: None,
19502                where_: None,
19503                group_by: None,
19504                group_by_all: false,
19505                having: None,
19506                unions: Vec::new(),
19507                order_by: Vec::new(),
19508                limit: None,
19509                offset: None,
19510                limit_with_ties: false,
19511                window_check_exprs: Vec::new(),
19512            };
19513            return Ok(TableRef {
19514                name: table_alias.clone(),
19515                alias: Some(table_alias),
19516                only: false,
19517                as_of_segment: None,
19518                unnest_expr: None,
19519                unnest_column_aliases: column_aliases,
19520                with_ordinality,
19521                generate_series_args: None,
19522                lateral_subquery: Some(Box::new(inner)),
19523                jsonb_each_text_arg: None,
19524                table_fn_call: None,
19525                rows_from: None,
19526                json_table: None,
19527                // regexp_matches returns text[], a base type: `SELECT m FROM
19528                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19529                scalar_fn_item: true,
19530            });
19531        }
19532        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19533        // / json_ variants as a FROM item. Rewritten into
19534        // `unnest(<same fn>(<expr>))`: the scalar form returns the
19535        // elements as a TEXT array, and the existing unnest SRF path
19536        // materialises one row per element. PG's natural column name
19537        // is `value`; an `AS a(col)` column-alias list overrides it.
19538        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19539                if s.eq_ignore_ascii_case("jsonb_array_elements")
19540                    || s.eq_ignore_ascii_case("json_array_elements")
19541                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19542                    || s.eq_ignore_ascii_case("json_array_elements_text")
19543                    || s.eq_ignore_ascii_case("jsonb_object_keys")
19544                    || s.eq_ignore_ascii_case("json_object_keys")
19545                    || s.eq_ignore_ascii_case("jsonb_path_query")
19546                    || s.eq_ignore_ascii_case("json_path_query")
19547                    || s.eq_ignore_ascii_case("generate_subscripts")
19548                    || s.eq_ignore_ascii_case("string_to_table")
19549                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
19550            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19551        {
19552            let fn_name = match self.peek() {
19553                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19554                _ => unreachable!(),
19555            };
19556            self.advance(); // fn name
19557            self.advance(); // (
19558            let mut fn_args: Vec<Expr> = Vec::new();
19559            loop {
19560                fn_args.push(self.parse_expr(0)?);
19561                if matches!(self.peek(), Token::Comma) {
19562                    self.advance();
19563                    continue;
19564                }
19565                break;
19566            }
19567            if !matches!(self.peek(), Token::RParen) {
19568                return Err(self.err(alloc::format!(
19569                    "expected ')' after {fn_name}() arguments, got {:?}",
19570                    self.peek()
19571                )));
19572            }
19573            self.advance();
19574            let with_ordinality = self.absorb_with_ordinality();
19575            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19576            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19577            // PG's natural column name: the array-elements SRFs
19578            // declare an OUT parameter `value`; jsonb_object_keys
19579            // and generate_subscripts have none, so the column is
19580            // named after the function. A bare table alias on a
19581            // single-column SRF renames the column too (PG: `FROM
19582            // generate_subscripts(a, 1) AS s` projects column s) —
19583            // except for the OUT-parameter SRFs, whose column stays
19584            // `value` under a bare alias.
19585            let natural_col = if fn_name.ends_with("_array_elements")
19586                || fn_name.ends_with("_array_elements_text")
19587            {
19588                "value".to_string()
19589            } else {
19590                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19591            };
19592            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19593            // Keep any further entries — the second names the
19594            // ordinality column under WITH ORDINALITY.
19595            srf_cols.extend(column_aliases.into_iter().skip(1));
19596            // The *_to_table SRFs are row-streams over the existing
19597            // *_to_array scalars — map the call target; the display
19598            // name (alias / column defaults) keeps the SRF spelling.
19599            let call_name = match fn_name.as_str() {
19600                "string_to_table" => "string_to_array".to_string(),
19601                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19602                _ => fn_name,
19603            };
19604            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19605            // preceding FROM item (bare or qualified column) is correlated;
19606            // route it through the per-outer-row lateral channel.
19607            let expr = crate::ast::Expr::FunctionCall {
19608                name: call_name,
19609                args: fn_args,
19610            };
19611            let correlated = Self::expr_has_any_column(&expr);
19612            let tref = TableRef {
19613                name,
19614                alias: alias_ident,
19615                only: false,
19616                as_of_segment: None,
19617                unnest_expr: Some(Box::new(expr)),
19618                unnest_column_aliases: srf_cols,
19619                with_ordinality,
19620                generate_series_args: None,
19621                lateral_subquery: None,
19622                jsonb_each_text_arg: None,
19623                table_fn_call: None,
19624                rows_from: None,
19625                json_table: None,
19626                // Each of these returns a BASE type (jsonb / text / int), so the item's
19627                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19628                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19629                scalar_fn_item: !with_ordinality,
19630            };
19631            return Ok(if correlated {
19632                Self::wrap_correlated_srf(tref)
19633            } else {
19634                tref
19635            });
19636        }
19637        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19638        // explicit parallel-zip syntax. Each entry lowers to its
19639        // array-returning scalar form (unnest(x) → x itself; the
19640        // FROM-SRF rewrite family → their scalar array calls) and
19641        // the list rides the multi-arg unnest zip channel:
19642        // NULL-padded to the longest, WITH ORDINALITY appends the
19643        // counter. generate_series has no scalar array form and
19644        // errors honestly.
19645        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
19646            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
19647            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19648        {
19649            self.advance(); // ROWS
19650            self.advance(); // FROM
19651            self.advance(); // (
19652            let mut entries: Vec<Expr> = Vec::new();
19653            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
19654            // Used only when some entry has no array form.
19655            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
19656            loop {
19657                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
19658                if !matches!(self.peek(), Token::LParen) {
19659                    return Err(self.err(alloc::format!(
19660                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
19661                        self.peek()
19662                    )));
19663                }
19664                self.advance();
19665                let mut fn_args: Vec<Expr> = Vec::new();
19666                if !matches!(self.peek(), Token::RParen) {
19667                    loop {
19668                        fn_args.push(self.parse_expr(0)?);
19669                        if matches!(self.peek(), Token::Comma) {
19670                            self.advance();
19671                            continue;
19672                        }
19673                        break;
19674                    }
19675                }
19676                if !matches!(self.peek(), Token::RParen) {
19677                    return Err(self.err(alloc::format!(
19678                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
19679                        self.peek()
19680                    )));
19681                }
19682                self.advance();
19683                let entry = match fn_name.as_str() {
19684                    "unnest" => {
19685                        if fn_args.len() != 1 {
19686                            return Err(
19687                                self.err("unnest inside ROWS FROM takes exactly one array".into())
19688                            );
19689                        }
19690                        fn_args.pop().expect("len checked")
19691                    }
19692                    "jsonb_array_elements"
19693                    | "json_array_elements"
19694                    | "jsonb_array_elements_text"
19695                    | "json_array_elements_text"
19696                    | "jsonb_object_keys"
19697                    | "json_object_keys"
19698                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
19699                        name: fn_name,
19700                        args: fn_args,
19701                    },
19702                    "string_to_table" => crate::ast::Expr::FunctionCall {
19703                        name: "string_to_array".to_string(),
19704                        args: fn_args,
19705                    },
19706                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
19707                        name: "regexp_split_to_array".to_string(),
19708                        args: fn_args,
19709                    },
19710                    // v7.39 (read01 round 74) — an SRF with no array form
19711                    // (`generate_series`, a user `RETURNS SETOF` function) has no
19712                    // scalar expression to zip, so the WHOLE list switches to the
19713                    // rows_from channel, which runs each function and zips the
19714                    // rows themselves. The all-array case keeps the old lowering:
19715                    // it is well-trodden and this must not disturb it.
19716                    _ => {
19717                        generic.push((fn_name, fn_args));
19718                        if matches!(self.peek(), Token::Comma) {
19719                            self.advance();
19720                            continue;
19721                        }
19722                        break;
19723                    }
19724                };
19725                generic.push((
19726                    // The array-able entries carry their lowered expr along, so a
19727                    // MIXED list still works: the engine sees the scalar array
19728                    // form and unnests it.
19729                    "__array".to_string(),
19730                    alloc::vec![entry.clone()],
19731                ));
19732                entries.push(entry);
19733                if matches!(self.peek(), Token::Comma) {
19734                    self.advance();
19735                    continue;
19736                }
19737                break;
19738            }
19739            if !matches!(self.peek(), Token::RParen) {
19740                return Err(self.err(alloc::format!(
19741                    "expected ')' to close ROWS FROM, got {:?}",
19742                    self.peek()
19743                )));
19744            }
19745            self.advance();
19746            let with_ordinality = self.absorb_with_ordinality();
19747            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19748            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
19749            // v7.39 (read01 round 74) — some entry had no array form, so the whole
19750            // list rides the generic channel.
19751            if generic.iter().any(|(n, _)| n != "__array") {
19752                let correlated = generic
19753                    .iter()
19754                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
19755                let tref = TableRef {
19756                    name,
19757                    alias: alias_ident,
19758                    only: false,
19759                    as_of_segment: None,
19760                    unnest_expr: None,
19761                    unnest_column_aliases,
19762                    with_ordinality,
19763                    generate_series_args: None,
19764                    lateral_subquery: None,
19765                    jsonb_each_text_arg: None,
19766                    table_fn_call: None,
19767                    rows_from: Some(generic),
19768                    json_table: None,
19769                    scalar_fn_item: false,
19770                };
19771                return Ok(if correlated {
19772                    Self::wrap_correlated_srf(tref)
19773                } else {
19774                    tref
19775                });
19776            }
19777            let correlated = entries.iter().any(Self::expr_has_any_column);
19778            let expr = if entries.len() == 1 {
19779                entries.pop().expect("len checked")
19780            } else {
19781                crate::ast::Expr::FunctionCall {
19782                    name: "__unnest_zip".to_string(),
19783                    args: entries,
19784                }
19785            };
19786            let tref = TableRef {
19787                name,
19788                alias: alias_ident,
19789                only: false,
19790                as_of_segment: None,
19791                unnest_expr: Some(Box::new(expr)),
19792                unnest_column_aliases,
19793                with_ordinality,
19794                generate_series_args: None,
19795                lateral_subquery: None,
19796                jsonb_each_text_arg: None,
19797                table_fn_call: None,
19798                rows_from: None,
19799                json_table: None,
19800                scalar_fn_item: false,
19801            };
19802            return Ok(if correlated {
19803                Self::wrap_correlated_srf(tref)
19804            } else {
19805                tref
19806            });
19807        }
19808        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
19809        // source. Detect at the head before the bare-ident fallback;
19810        // unnest is not a reserved token.
19811        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
19812            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19813        {
19814            self.advance(); // unnest
19815            self.advance(); // (
19816            let mut srf_args = alloc::vec![self.parse_expr(0)?];
19817            while matches!(self.peek(), Token::Comma) {
19818                self.advance();
19819                srf_args.push(self.parse_expr(0)?);
19820            }
19821            if !matches!(self.peek(), Token::RParen) {
19822                return Err(self.err(alloc::format!(
19823                    "expected ')' after unnest() argument, got {:?}",
19824                    self.peek()
19825                )));
19826            }
19827            self.advance();
19828            // Multi-arg unnest(a, b, …) zips the arrays in
19829            // parallel, NULL-padding to the longest (PG's ROWS
19830            // FROM shorthand). Lower onto the unnest channel as an
19831            // internal marker call the executors unpack.
19832            let expr = if srf_args.len() == 1 {
19833                srf_args.pop().expect("len checked")
19834            } else {
19835                crate::ast::Expr::FunctionCall {
19836                    name: "__unnest_zip".to_string(),
19837                    args: srf_args,
19838                }
19839            };
19840            let with_ordinality = self.absorb_with_ordinality();
19841            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
19842            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
19843            let correlated = Self::expr_has_any_column(&expr);
19844            let tref = TableRef {
19845                name,
19846                alias: alias_ident,
19847                only: false,
19848                as_of_segment: None,
19849                unnest_expr: Some(Box::new(expr)),
19850                unnest_column_aliases,
19851                with_ordinality,
19852                generate_series_args: None,
19853                lateral_subquery: None,
19854                jsonb_each_text_arg: None,
19855                table_fn_call: None,
19856                rows_from: None,
19857                json_table: None,
19858                scalar_fn_item: false,
19859            };
19860            return Ok(if correlated {
19861                Self::wrap_correlated_srf(tref)
19862            } else {
19863                tref
19864            });
19865        }
19866        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
19867        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
19868        // generic table-fn arg parser can't read), so it is intercepted
19869        // here BEFORE the generic dispatch. The doc expr may reference
19870        // outer columns (implicit LATERAL) — same correlated-wrap rule.
19871        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19872                if s.eq_ignore_ascii_case("json_table"))
19873            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19874        {
19875            let tref = self.parse_json_table_ref()?;
19876            let correlated = tref
19877                .json_table
19878                .as_deref()
19879                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
19880            return Ok(if correlated {
19881                Self::wrap_correlated_srf(tref)
19882            } else {
19883                tref
19884            });
19885        }
19886        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
19887        // functions dispatched by name (`pg_partition_tree('t')`,
19888        // `pg_partition_ancestors('t')`). Same head-detection shape as
19889        // unnest; the engine executor owns the row shape per function.
19890        // v7.39 (read01 round 65) — and a USER function in FROM position
19891        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
19892        // (generate_series / unnest / the json_each family) keep it — their arms
19893        // sit further down, so they are excluded here by name rather than by
19894        // ordering. Anything else that is an ident followed by `(` is a table
19895        // function; the engine executor decides whether it is a builtin, a
19896        // set-returning user function, or an error.
19897        // 7.38.1 S5.1 — pg_dump spells its table functions
19898        // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
19899        // strip the pg_catalog prefix here so the same head-detection
19900        // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
19901        // meaning.
19902        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
19903            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19904            && matches!(
19905                self.tokens.get(self.pos + 2),
19906                Some(Token::Ident(_) | Token::QuotedIdent(_))
19907            )
19908            && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
19909        {
19910            self.advance(); // pg_catalog
19911            self.advance(); // .
19912        }
19913        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19914                if !s.eq_ignore_ascii_case("generate_series")
19915                    && !s.eq_ignore_ascii_case("unnest")
19916                    && !is_json_each_name(s))
19917            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19918        {
19919            // Body out-of-line — this parse sits on the FROM/subquery
19920            // recursion chain (debug frame-cliff discipline).
19921            // v7.39 (read01 round 69) — a call whose arguments reference an outer
19922            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
19923            // outer row, so it rides the lateral channel. Same rule the unnest
19924            // arm uses.
19925            let tref = self.parse_table_fn_ref()?;
19926            let correlated = tref
19927                .table_fn_call
19928                .as_deref()
19929                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
19930            return Ok(if correlated {
19931                Self::wrap_correlated_srf(tref)
19932            } else {
19933                tref
19934            });
19935        }
19936        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
19937        // [, step])` set-returning source. Same shape as unnest:
19938        // detect at the head, parse the comma-separated arg list,
19939        // dispatch downstream through the engine's set-returning
19940        // path. Supports integer triplets (mailrs's `WITH row_no AS
19941        // (SELECT * FROM generate_series(1, N))` pattern) and
19942        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
19943        // date-range iteration pattern, which pre-3.10 had no
19944        // direct equivalent in SPG).
19945        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
19946            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19947        {
19948            self.advance(); // generate_series
19949            self.advance(); // (
19950            let mut args: Vec<Expr> = Vec::new();
19951            loop {
19952                args.push(self.parse_expr(0)?);
19953                if matches!(self.peek(), Token::Comma) {
19954                    self.advance();
19955                    continue;
19956                }
19957                break;
19958            }
19959            if !matches!(self.peek(), Token::RParen) {
19960                return Err(self.err(alloc::format!(
19961                    "expected ')' after generate_series() arguments, got {:?}",
19962                    self.peek()
19963                )));
19964            }
19965            self.advance();
19966            if args.len() < 2 || args.len() > 3 {
19967                return Err(self.err(alloc::format!(
19968                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
19969                    args.len()
19970                )));
19971            }
19972            let with_ordinality = self.absorb_with_ordinality();
19973            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19974            let name = alias_ident
19975                .clone()
19976                .unwrap_or_else(|| "generate_series".to_string());
19977            let correlated = args.iter().any(Self::expr_has_any_column);
19978            let tref = TableRef {
19979                name,
19980                alias: alias_ident,
19981                only: false,
19982                as_of_segment: None,
19983                unnest_expr: None,
19984                unnest_column_aliases: column_aliases,
19985                with_ordinality,
19986                generate_series_args: Some(args),
19987                lateral_subquery: None,
19988                jsonb_each_text_arg: None,
19989                table_fn_call: None,
19990                rows_from: None,
19991                json_table: None,
19992                scalar_fn_item: false,
19993            };
19994            return Ok(if correlated {
19995                Self::wrap_correlated_srf(tref)
19996            } else {
19997                tref
19998            });
19999        }
20000        // v7.16.2 — preserve information_schema / pg_catalog
20001        // qualifiers (mailrs round-10 A.3). The generic
20002        // `expect_ident_like` strip silently drops the schema;
20003        // we want the engine to recognise these PG meta tables
20004        // and synthesise rows from the live catalog. Produce a
20005        // synthetic name (`__spg_info_columns` etc.) so the
20006        // engine's SELECT-side router can dispatch without
20007        // clashing with any user-defined `columns` table.
20008        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
20009            (synth, Some(orig))
20010        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
20011            (synth, Some(orig))
20012        } else {
20013            (self.expect_ident_like()?, None)
20014        };
20015        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
20016        // time-travel clause. Parse BEFORE the alias so the
20017        // alias can still ride at the tail (`tbl AS OF SEGMENT
20018        // '5' alias`). `AS` is a reserved keyword token, while
20019        // `OF` and `SEGMENT` are bare idents.
20020        let as_of_segment = if matches!(self.peek(), Token::As)
20021            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
20022        {
20023            self.advance(); // AS
20024            self.advance(); // OF
20025            let kw = match self.peek().clone() {
20026                Token::Ident(s) | Token::QuotedIdent(s) => s,
20027                other => {
20028                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
20029                }
20030            };
20031            if !kw.eq_ignore_ascii_case("segment") {
20032                return Err(self.err(format!(
20033                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
20034                )));
20035            }
20036            self.advance();
20037            // Segment id literal — accept either a string or
20038            // integer for operator ergonomics.
20039            let id = match self.advance() {
20040                Token::String(s) => s
20041                    .parse::<u32>()
20042                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20043                Token::Integer(n) => u32::try_from(n)
20044                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20045                other => {
20046                    return Err(self.err(format!(
20047                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
20048                    )));
20049                }
20050            };
20051            Some(id)
20052        } else {
20053            None
20054        };
20055        // TABLESAMPLE is not a reserved token — keep the bare-ident
20056        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20057        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20058        {
20059            None
20060        } else {
20061            self.parse_optional_alias()?
20062        };
20063        // r1052 — a catalog name rewritten to its synthetic form keeps
20064        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20065        // still binds after `pg_cast` became `__spg_pg_cast`. PG
20066        // semantics: the visible name of `pg_catalog.pg_cast` IS
20067        // `pg_cast`. Without this, every table-name-qualified column
20068        // on a synthesised catalog answered "missing FROM-clause
20069        // entry" — which is the wall pg_dump hit on its first
20070        // pg_proc/pg_cast query.
20071        let alias = match (&alias, &meta_original) {
20072            (None, Some(orig)) if *orig != name => Some(orig.clone()),
20073            _ => alias,
20074        };
20075        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20076        // (PG grammar). BERNOULLI lowers to a per-row
20077        // `random() < p/100` conjunct on the enclosing SELECT's
20078        // WHERE — exact row-level Bernoulli semantics. SYSTEM
20079        // shares the lowering: SPG has no page structure to
20080        // sample, and the row-level form returns the same expected
20081        // fraction. REPEATABLE(seed) promises a deterministic
20082        // sample SPG cannot honour yet — honest error rather than
20083        // a silently ignored seed.
20084        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20085            self.advance();
20086            let method = self.expect_ident_like()?;
20087            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20088                return Err(self.err(alloc::format!(
20089                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20090                )));
20091            }
20092            if !matches!(self.peek(), Token::LParen) {
20093                return Err(self.err(alloc::format!(
20094                    "expected '(' after TABLESAMPLE {}, got {:?}",
20095                    method.to_ascii_uppercase(),
20096                    self.peek()
20097                )));
20098            }
20099            self.advance();
20100            let percent = self.parse_expr(0)?;
20101            if !matches!(self.peek(), Token::RParen) {
20102                return Err(self.err(alloc::format!(
20103                    "expected ')' after TABLESAMPLE percentage, got {:?}",
20104                    self.peek()
20105                )));
20106            }
20107            self.advance();
20108            // REPEATABLE(seed) → a deterministic per-row draw seeded by
20109            // `seed`, so the sample is stable across repeats and rescans.
20110            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20111            let mut sample_seed: Option<Expr> = None;
20112            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20113                self.advance();
20114                if !matches!(self.peek(), Token::LParen) {
20115                    return Err(self.err(alloc::format!(
20116                        "expected '(' after REPEATABLE, got {:?}",
20117                        self.peek()
20118                    )));
20119                }
20120                self.advance();
20121                let seed = self.parse_expr(0)?;
20122                if !matches!(self.peek(), Token::RParen) {
20123                    return Err(self.err(alloc::format!(
20124                        "expected ')' after REPEATABLE seed, got {:?}",
20125                        self.peek()
20126                    )));
20127                }
20128                self.advance();
20129                sample_seed = Some(seed);
20130            }
20131            let draw = match sample_seed {
20132                Some(seed) => Expr::FunctionCall {
20133                    name: "__tsm_fract".to_string(),
20134                    args: alloc::vec![seed],
20135                },
20136                None => Expr::FunctionCall {
20137                    name: "random".to_string(),
20138                    args: Vec::new(),
20139                },
20140            };
20141            self.pending_sample_preds.push(Expr::Binary {
20142                lhs: Box::new(draw),
20143                op: crate::ast::BinOp::Lt,
20144                rhs: Box::new(Expr::Binary {
20145                    lhs: Box::new(percent),
20146                    op: crate::ast::BinOp::Div,
20147                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20148                }),
20149            });
20150        }
20151        Ok(TableRef {
20152            name,
20153            alias,
20154            only,
20155            as_of_segment,
20156            unnest_expr: None,
20157            unnest_column_aliases: Vec::new(),
20158            with_ordinality: false,
20159            generate_series_args: None,
20160            lateral_subquery: None,
20161            jsonb_each_text_arg: None,
20162            table_fn_call: None,
20163            rows_from: None,
20164            json_table: None,
20165            scalar_fn_item: false,
20166        })
20167    }
20168
20169    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20170    /// but also accepts `AS alias(col [, col, …])` — the
20171    /// PG-standard table-function column-list form. The column
20172    /// list is only honoured when paired with `UNNEST(...)` in
20173    /// the parent; other call sites currently discard it.
20174    /// True when the expression tree contains a qualified column
20175    /// reference (`t.col`) — the syntactic marker that an SRF
20176    /// argument correlates with a preceding FROM item.
20177    fn expr_has_qualified_column(e: &Expr) -> bool {
20178        match e {
20179            Expr::Column(c) => c.qualifier.is_some(),
20180            Expr::Binary { lhs, rhs, .. } => {
20181                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20182            }
20183            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20184            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20185            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20186            Expr::Case {
20187                operand,
20188                branches,
20189                else_branch,
20190            } => {
20191                operand
20192                    .as_deref()
20193                    .is_some_and(Self::expr_has_qualified_column)
20194                    || branches.iter().any(|(w, t)| {
20195                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20196                    })
20197                    || else_branch
20198                        .as_deref()
20199                        .is_some_and(Self::expr_has_qualified_column)
20200            }
20201            _ => false,
20202        }
20203    }
20204
20205    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20206    /// counts a bare (unqualified) column. A set-returning function has no
20207    /// input columns of its own, so ANY column in its arguments is an outer
20208    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20209    fn expr_has_any_column(e: &Expr) -> bool {
20210        match e {
20211            Expr::Column(_) => true,
20212            Expr::Binary { lhs, rhs, .. } => {
20213                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20214            }
20215            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20216            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20217            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20218            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20219            // constructor or subscript fell to the `_ => false` arm, so
20220            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20221            // channel and the eager peer eval answered `column "x" does
20222            // not exist` (the substitution walker already recurses both
20223            // shapes; only this detector was blind to them).
20224            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20225            Expr::ArraySubscript { target, index } => {
20226                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20227            }
20228            Expr::Case {
20229                operand,
20230                branches,
20231                else_branch,
20232            } => {
20233                operand.as_deref().is_some_and(Self::expr_has_any_column)
20234                    || branches
20235                        .iter()
20236                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20237                    || else_branch
20238                        .as_deref()
20239                        .is_some_and(Self::expr_has_any_column)
20240            }
20241            _ => false,
20242        }
20243    }
20244
20245    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20246    /// `generate_series(1, t.n)`) into the lateral_subquery
20247    /// channel: `SELECT * FROM <srf>` executes per outer row with
20248    /// outer references substituted (v7.37.43-T4.5 machinery).
20249    /// Uncorrelated SRFs stay on their plain channels.
20250    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20251        let name = srf.name.clone();
20252        let alias = srf.alias.clone();
20253        let inner = crate::ast::SelectStatement {
20254            locking: None,
20255            ctes: Vec::new(),
20256            distinct: false,
20257            distinct_on: Vec::new(),
20258            items: alloc::vec![crate::ast::SelectItem::Wildcard],
20259            from: Some(crate::ast::FromClause {
20260                primary: srf,
20261                joins: Vec::new(),
20262            }),
20263            where_: None,
20264            group_by: None,
20265            group_by_all: false,
20266            having: None,
20267            unions: Vec::new(),
20268            order_by: Vec::new(),
20269            limit: None,
20270            offset: None,
20271            limit_with_ties: false,
20272            window_check_exprs: Vec::new(),
20273        };
20274        TableRef {
20275            name,
20276            alias,
20277            only: false,
20278            as_of_segment: None,
20279            unnest_expr: None,
20280            unnest_column_aliases: Vec::new(),
20281            with_ordinality: false,
20282            generate_series_args: None,
20283            lateral_subquery: Some(Box::new(inner)),
20284            jsonb_each_text_arg: None,
20285            table_fn_call: None,
20286            rows_from: None,
20287            json_table: None,
20288            scalar_fn_item: false,
20289        }
20290    }
20291
20292    /// True when the expression tree contains an unresolved
20293    /// `OVER w` marker (see parse_over_clause).
20294    fn expr_has_named_window(e: &Expr) -> bool {
20295        match e {
20296            Expr::WindowFunction { partition_by, .. } => matches!(
20297                partition_by.as_slice(),
20298                [Expr::Column(c)] if matches!(
20299                    c.qualifier.as_deref(),
20300                    Some("__named_window__") | Some("__named_window_ref__")
20301                )
20302            ),
20303            Expr::Binary { lhs, rhs, .. } => {
20304                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20305            }
20306            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20307            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20308            Expr::Case {
20309                operand,
20310                branches,
20311                else_branch,
20312            } => {
20313                operand.as_deref().is_some_and(Self::expr_has_named_window)
20314                    || branches.iter().any(|(w, t)| {
20315                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20316                    })
20317                    || else_branch
20318                        .as_deref()
20319                        .is_some_and(Self::expr_has_named_window)
20320            }
20321            _ => false,
20322        }
20323    }
20324
20325    /// v7.39 (round 705) — the NAMES the expression references through the
20326    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20327    /// definitions nothing referenced. Traversal mirrors
20328    /// `expr_has_named_window` above.
20329    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20330        match e {
20331            Expr::WindowFunction { partition_by, .. } => {
20332                if let [Expr::Column(c)] = partition_by.as_slice()
20333                    && matches!(
20334                        c.qualifier.as_deref(),
20335                        Some("__named_window__") | Some("__named_window_ref__")
20336                    )
20337                {
20338                    into.push(c.name.clone());
20339                }
20340            }
20341            Expr::Binary { lhs, rhs, .. } => {
20342                Self::collect_named_window_refs(lhs, into);
20343                Self::collect_named_window_refs(rhs, into);
20344            }
20345            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20346                Self::collect_named_window_refs(expr, into);
20347            }
20348            Expr::FunctionCall { args, .. } => {
20349                for a in args {
20350                    Self::collect_named_window_refs(a, into);
20351                }
20352            }
20353            Expr::Case {
20354                operand,
20355                branches,
20356                else_branch,
20357            } => {
20358                if let Some(o) = operand.as_deref() {
20359                    Self::collect_named_window_refs(o, into);
20360                }
20361                for (w, t) in branches {
20362                    Self::collect_named_window_refs(w, into);
20363                    Self::collect_named_window_refs(t, into);
20364                }
20365                if let Some(eb) = else_branch.as_deref() {
20366                    Self::collect_named_window_refs(eb, into);
20367                }
20368            }
20369            _ => {}
20370        }
20371    }
20372
20373    /// Inline named-window definitions into the `OVER w` markers.
20374    /// An unknown name errors (PG: window "w" does not exist).
20375    #[allow(clippy::type_complexity)]
20376    fn substitute_named_windows(
20377        e: &mut Expr,
20378        defs: &[(
20379            String,
20380            (
20381                Vec<Expr>,
20382                Vec<(Expr, bool, Option<bool>)>,
20383                Option<WindowFrame>,
20384            ),
20385        )],
20386    ) -> Result<(), String> {
20387        match e {
20388            Expr::WindowFunction {
20389                partition_by,
20390                order_by,
20391                frame,
20392                ..
20393            } => {
20394                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20395                // from the bare `OVER w1` (a plain reference).
20396                let named = match partition_by.as_slice() {
20397                    [Expr::Column(c)] => match c.qualifier.as_deref() {
20398                        Some("__named_window__") => Some((c.name.clone(), false)),
20399                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
20400                        _ => None,
20401                    },
20402                    _ => None,
20403                };
20404                if let Some((wname, is_copy)) = named {
20405                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20406                    else {
20407                        return Err(alloc::format!("window {wname:?} does not exist"));
20408                    };
20409                    if !is_copy {
20410                        *partition_by = def.0.clone();
20411                        *order_by = def.1.clone();
20412                        *frame = def.2.clone();
20413                        return Ok(());
20414                    }
20415                    // v7.39 (round 229) — PG's copy rules, probed against
20416                    // 18.4: a copy inherits the partitioning, may supply an
20417                    // ordering only when the base has none, and may not copy
20418                    // a base that already carries a frame (its own frame
20419                    // would be ambiguous with the inherited one).
20420                    if !def.1.is_empty() && !order_by.is_empty() {
20421                        return Err(alloc::format!(
20422                            "cannot override ORDER BY clause of window \"{wname}\""
20423                        ));
20424                    }
20425                    if def.2.is_some() {
20426                        return Err(alloc::format!(
20427                            "cannot copy window \"{wname}\" because it has a frame clause"
20428                        ));
20429                    }
20430                    *partition_by = def.0.clone();
20431                    if order_by.is_empty() {
20432                        *order_by = def.1.clone();
20433                    }
20434                }
20435                Ok(())
20436            }
20437            Expr::Binary { lhs, rhs, .. } => {
20438                Self::substitute_named_windows(lhs, defs)?;
20439                Self::substitute_named_windows(rhs, defs)
20440            }
20441            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20442                Self::substitute_named_windows(expr, defs)
20443            }
20444            Expr::FunctionCall { args, .. } => {
20445                for a in args {
20446                    Self::substitute_named_windows(a, defs)?;
20447                }
20448                Ok(())
20449            }
20450            Expr::Case {
20451                operand,
20452                branches,
20453                else_branch,
20454            } => {
20455                if let Some(op) = operand {
20456                    Self::substitute_named_windows(op, defs)?;
20457                }
20458                for (w, t) in branches {
20459                    Self::substitute_named_windows(w, defs)?;
20460                    Self::substitute_named_windows(t, defs)?;
20461                }
20462                if let Some(el) = else_branch {
20463                    Self::substitute_named_windows(el, defs)?;
20464                }
20465                Ok(())
20466            }
20467            _ => Ok(()),
20468        }
20469    }
20470
20471    /// SQL-standard `TABLE name` shorthand — builds the equivalent
20472    /// `SELECT * FROM name` head. Callers own set-op chain / tail
20473    /// composition.
20474    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20475        debug_assert!(matches!(self.peek(), Token::Table));
20476        self.advance(); // TABLE
20477        let tname = self.expect_ident_like()?;
20478        Ok(SelectStatement {
20479            locking: None,
20480            ctes: Vec::new(),
20481            distinct: false,
20482            distinct_on: Vec::new(),
20483            items: alloc::vec![SelectItem::Wildcard],
20484            from: Some(FromClause {
20485                primary: TableRef {
20486                    name: tname,
20487                    alias: None,
20488                    only: false,
20489                    as_of_segment: None,
20490                    unnest_expr: None,
20491                    unnest_column_aliases: Vec::new(),
20492                    with_ordinality: false,
20493                    generate_series_args: None,
20494                    lateral_subquery: None,
20495                    jsonb_each_text_arg: None,
20496                    table_fn_call: None,
20497                    rows_from: None,
20498                    json_table: None,
20499                    scalar_fn_item: false,
20500                },
20501                joins: Vec::new(),
20502            }),
20503            where_: None,
20504            group_by: None,
20505            group_by_all: false,
20506            having: None,
20507            unions: Vec::new(),
20508            order_by: Vec::new(),
20509            limit: None,
20510            offset: None,
20511            limit_with_ties: false,
20512            window_check_exprs: Vec::new(),
20513        })
20514    }
20515
20516    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20517    /// variants) → a derived table that reads each declared column out of
20518    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20519    /// `jsonb_array_elements(J)` (one row per element, column `value`);
20520    /// the scalar *record form projects a single row straight off `J`.
20521    /// Rides the existing lateral-subquery channel, so no new executor or
20522    /// AST is needed.
20523    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20524        use crate::ast::{
20525            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20526        };
20527        let fn_name = match self.peek() {
20528            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20529            _ => unreachable!("caller guarded is_json_to_record_name"),
20530        };
20531        self.advance(); // fn name
20532        self.advance(); // (
20533        let mut arg = self.parse_expr(0)?;
20534        // populate_record(base, json): the base only carries the record
20535        // type here — the JSON argument is the second expression.
20536        let mut base: Option<Expr> = None;
20537        if matches!(self.peek(), Token::Comma) {
20538            self.advance();
20539            base = Some(arg);
20540            arg = self.parse_expr(0)?;
20541        }
20542        if !matches!(self.peek(), Token::RParen) {
20543            return Err(self.err(alloc::format!(
20544                "expected ')' after {fn_name}() argument, got {:?}",
20545                self.peek()
20546            )));
20547        }
20548        self.advance(); // )
20549        let is_set = fn_name.ends_with("recordset");
20550        // `[AS] alias ( col type [, …] )` column-definition list.
20551        if matches!(self.peek(), Token::As) {
20552            self.advance();
20553        }
20554        let alias_opt = match self.peek() {
20555            Token::Ident(s) | Token::QuotedIdent(s) => {
20556                let a = s.clone();
20557                self.advance();
20558                Some(a)
20559            }
20560            _ => None,
20561        };
20562        // v7.39 (read01 round 76) — the populate family's canonical PG
20563        // spelling carries no column list at all: the row shape comes from
20564        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20565        // j)`). The parser has no catalog, so hand the two arguments to the
20566        // engine's table-function channel, which does. Only `*_to_record*`
20567        // (whose base is bare `record`) genuinely requires the list.
20568        if !matches!(self.peek(), Token::LParen) {
20569            if let Some(base_expr) = base {
20570                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20571                return Ok(TableRef {
20572                    name: alias.clone(),
20573                    alias: Some(alias),
20574                    only: false,
20575                    as_of_segment: None,
20576                    unnest_expr: None,
20577                    unnest_column_aliases: Vec::new(),
20578                    with_ordinality: false,
20579                    generate_series_args: None,
20580                    lateral_subquery: None,
20581                    jsonb_each_text_arg: None,
20582                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20583                    rows_from: None,
20584                    json_table: None,
20585                    scalar_fn_item: false,
20586                });
20587            }
20588            return Err(self.err(alloc::format!(
20589                "expected '(' to start the {fn_name} column-definition list, got {:?}",
20590                self.peek()
20591            )));
20592        }
20593        let Some(alias) = alias_opt else {
20594            return Err(self.err(alloc::format!(
20595                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20596            )));
20597        };
20598        self.advance(); // (
20599        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20600        loop {
20601            let col = self.expect_ident_like()?;
20602            let ty = self.parse_cast_target()?;
20603            coldefs.push((col, ty));
20604            if matches!(self.peek(), Token::Comma) {
20605                self.advance();
20606                continue;
20607            }
20608            if matches!(self.peek(), Token::RParen) {
20609                self.advance();
20610                break;
20611            }
20612            return Err(self.err(alloc::format!(
20613                "expected ',' or ')' in {fn_name} column list, got {:?}",
20614                self.peek()
20615            )));
20616        }
20617        if coldefs.is_empty() {
20618            return Err(self.err(alloc::format!(
20619                "{fn_name} column-definition list must declare at least one column"
20620            )));
20621        }
20622        // Per column: (base ->> 'col')::type AS col. The base is the
20623        // per-element `value` column for the *set form, or the argument
20624        // itself for the scalar record form.
20625        let items: Vec<SelectItem> = coldefs
20626            .into_iter()
20627            .map(|(col, ty)| {
20628                let base = if is_set {
20629                    Expr::Column(ColumnName {
20630                        qualifier: None,
20631                        name: "value".to_string(),
20632                    })
20633                } else {
20634                    arg.clone()
20635                };
20636                SelectItem::Expr {
20637                    expr: Expr::Cast {
20638                        expr: Box::new(Expr::Binary {
20639                            lhs: Box::new(base),
20640                            op: BinOp::JsonGetText,
20641                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
20642                        }),
20643                        target: ty,
20644                    },
20645                    alias: Some(col),
20646                }
20647            })
20648            .collect();
20649        let from = if is_set {
20650            let elem_fn = if fn_name.starts_with("jsonb") {
20651                "jsonb_array_elements"
20652            } else {
20653                "json_array_elements"
20654            };
20655            Some(FromClause {
20656                primary: TableRef {
20657                    name: "value".to_string(),
20658                    alias: None,
20659                    only: false,
20660                    as_of_segment: None,
20661                    unnest_expr: Some(Box::new(Expr::FunctionCall {
20662                        name: elem_fn.to_string(),
20663                        args: alloc::vec![arg],
20664                    })),
20665                    unnest_column_aliases: alloc::vec!["value".to_string()],
20666                    with_ordinality: false,
20667                    generate_series_args: None,
20668                    lateral_subquery: None,
20669                    jsonb_each_text_arg: None,
20670                    table_fn_call: None,
20671                    rows_from: None,
20672                    json_table: None,
20673                    scalar_fn_item: false,
20674                },
20675                joins: Vec::new(),
20676            })
20677        } else {
20678            None
20679        };
20680        let inner = SelectStatement {
20681            locking: None,
20682            ctes: Vec::new(),
20683            distinct: false,
20684            distinct_on: Vec::new(),
20685            items,
20686            from,
20687            where_: None,
20688            group_by: None,
20689            group_by_all: false,
20690            having: None,
20691            unions: Vec::new(),
20692            order_by: Vec::new(),
20693            limit: None,
20694            offset: None,
20695            limit_with_ties: false,
20696            window_check_exprs: Vec::new(),
20697        };
20698        Ok(TableRef {
20699            name: alias.clone(),
20700            alias: Some(alias),
20701            only: false,
20702            as_of_segment: None,
20703            unnest_expr: None,
20704            unnest_column_aliases: Vec::new(),
20705            with_ordinality: false,
20706            generate_series_args: None,
20707            lateral_subquery: Some(Box::new(inner)),
20708            jsonb_each_text_arg: None,
20709            table_fn_call: None,
20710            rows_from: None,
20711            json_table: None,
20712            scalar_fn_item: false,
20713        })
20714    }
20715
20716    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
20717    /// Returns true when the clause was present. `WITH` alone (a
20718    /// CTE can never start here) is not enough — the ORDINALITY
20719    /// ident must follow, so a stray WITH still errors downstream.
20720    fn absorb_with_ordinality(&mut self) -> bool {
20721        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
20722            && matches!(self.tokens.get(self.pos + 1),
20723                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
20724        {
20725            self.advance();
20726            self.advance();
20727            true
20728        } else {
20729            false
20730        }
20731    }
20732
20733    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
20734    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
20735    /// Out-of-line: the caller sits on the FROM recursion chain.
20736    #[inline(never)]
20737    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
20738        let fn_name = match self.advance() {
20739            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20740            _ => unreachable!("caller peeked an ident"),
20741        };
20742        self.advance(); // (
20743        let mut args: Vec<Expr> = Vec::new();
20744        if !matches!(self.peek(), Token::RParen) {
20745            loop {
20746                args.push(self.parse_expr(0)?);
20747                if matches!(self.peek(), Token::Comma) {
20748                    self.advance();
20749                    continue;
20750                }
20751                break;
20752            }
20753        }
20754        if !matches!(self.peek(), Token::RParen) {
20755            return Err(self.err(alloc::format!(
20756                "expected ')' after {fn_name}() arguments, got {:?}",
20757                self.peek()
20758            )));
20759        }
20760        self.advance();
20761        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
20762        // counter column rides after the function's own, and the alias list
20763        // names it.
20764        let with_ordinality = self.absorb_with_ordinality();
20765        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20766        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
20767        Ok(TableRef {
20768            name,
20769            alias: alias_ident,
20770            only: false,
20771            as_of_segment: None,
20772            unnest_expr: None,
20773            unnest_column_aliases,
20774            with_ordinality,
20775            generate_series_args: None,
20776            lateral_subquery: None,
20777            jsonb_each_text_arg: None,
20778            table_fn_call: Some(Box::new((fn_name, args))),
20779            rows_from: None,
20780            json_table: None,
20781            scalar_fn_item: false,
20782        })
20783    }
20784
20785    /// v7.39 (round 205, JSON_TABLE) — parse
20786    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
20787    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
20788    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
20789    #[inline(never)]
20790    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
20791        self.advance(); // json_table
20792        self.advance(); // (
20793        let doc = Box::new(self.parse_expr(0)?);
20794        self.expect_comma_json_table()?;
20795        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
20796        // Optional `PASSING <expr> AS <name> [, …]`.
20797        let mut passing: Vec<(String, Expr)> = Vec::new();
20798        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
20799            self.advance();
20800            loop {
20801                let e = self.parse_expr(0)?;
20802                if !matches!(self.peek(), Token::As) {
20803                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
20804                }
20805                self.advance();
20806                let vname = match self.advance() {
20807                    Token::Ident(s) | Token::QuotedIdent(s) => s,
20808                    other => {
20809                        return Err(self.err(alloc::format!(
20810                            "expected PASSING variable name, got {other:?}"
20811                        )));
20812                    }
20813                };
20814                passing.push((vname, e));
20815                if matches!(self.peek(), Token::Comma) {
20816                    self.advance();
20817                    continue;
20818                }
20819                break;
20820            }
20821        }
20822        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20823            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
20824        }
20825        self.advance();
20826        let columns = self.parse_json_table_columns()?;
20827        if !matches!(self.peek(), Token::RParen) {
20828            return Err(self.err(alloc::format!(
20829                "expected ')' to close JSON_TABLE, got {:?}",
20830                self.peek()
20831            )));
20832        }
20833        self.advance();
20834        let alias_ident = self.parse_optional_alias()?;
20835        let name = alias_ident
20836            .clone()
20837            .unwrap_or_else(|| String::from("json_table"));
20838        Ok(TableRef {
20839            name,
20840            alias: alias_ident,
20841            only: false,
20842            as_of_segment: None,
20843            unnest_expr: None,
20844            unnest_column_aliases: Vec::new(),
20845            with_ordinality: false,
20846            generate_series_args: None,
20847            lateral_subquery: None,
20848            jsonb_each_text_arg: None,
20849            table_fn_call: None,
20850            rows_from: None,
20851            json_table: Some(Box::new(crate::ast::JsonTable {
20852                doc,
20853                row_path,
20854                columns,
20855                passing,
20856            })),
20857            scalar_fn_item: false,
20858        })
20859    }
20860
20861    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
20862        if !matches!(self.peek(), Token::Comma) {
20863            return Err(self.err(alloc::format!(
20864                "expected ',' after JSON_TABLE document, got {:?}",
20865                self.peek()
20866            )));
20867        }
20868        self.advance();
20869        Ok(())
20870    }
20871
20872    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
20873        match self.advance() {
20874            Token::String(s) => Ok(s),
20875            other => Err(self.err(alloc::format!(
20876                "expected {what} string literal, got {other:?}"
20877            ))),
20878        }
20879    }
20880
20881    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
20882    #[inline(never)]
20883    fn parse_json_table_columns(
20884        &mut self,
20885    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
20886        if !matches!(self.peek(), Token::LParen) {
20887            return Err(self.err("expected '(' after COLUMNS".into()));
20888        }
20889        self.advance();
20890        let mut cols = Vec::new();
20891        loop {
20892            cols.push(self.parse_json_table_one_column()?);
20893            if matches!(self.peek(), Token::Comma) {
20894                self.advance();
20895                continue;
20896            }
20897            break;
20898        }
20899        if !matches!(self.peek(), Token::RParen) {
20900            return Err(self.err(alloc::format!(
20901                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
20902                self.peek()
20903            )));
20904        }
20905        self.advance();
20906        Ok(cols)
20907    }
20908
20909    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
20910        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
20911        // NESTED [PATH] '<p>' COLUMNS (...)
20912        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
20913            self.advance();
20914            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20915                self.advance();
20916            }
20917            let path = self.parse_json_string_literal("NESTED PATH")?;
20918            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
20919                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
20920            }
20921            self.advance();
20922            let columns = self.parse_json_table_columns()?;
20923            return Ok(JsonTableColumn::Nested { path, columns });
20924        }
20925        // <name> ...
20926        let name = match self.advance() {
20927            Token::Ident(s) | Token::QuotedIdent(s) => s,
20928            other => {
20929                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
20930            }
20931        };
20932        // <name> FOR ORDINALITY
20933        if matches!(self.peek(), Token::For) {
20934            self.advance();
20935            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
20936                return Err(self.err("expected ORDINALITY after FOR".into()));
20937            }
20938            self.advance();
20939            return Ok(JsonTableColumn::Ordinality { name });
20940        }
20941        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
20942        let ty = self.parse_column_type_name()?;
20943        let mut format_json = false;
20944        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20945            self.advance();
20946            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20947                return Err(self.err("expected JSON after FORMAT".into()));
20948            }
20949            self.advance();
20950            format_json = true;
20951        }
20952        let mut exists = false;
20953        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
20954            self.advance();
20955            exists = true;
20956        }
20957        let mut path = alloc::format!("$.{name}");
20958        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
20959            self.advance();
20960            path = self.parse_json_string_literal("column PATH")?;
20961        }
20962        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
20963            // `FORMAT JSON` after PATH (alternate placement).
20964            self.advance();
20965            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
20966                self.advance();
20967            }
20968            format_json = true;
20969        }
20970        let mut wrapper = false;
20971        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
20972            self.advance();
20973            // optional CONDITIONAL/UNCONDITIONAL
20974            if matches!(self.peek(), Token::Ident(s)
20975                if s.eq_ignore_ascii_case("unconditional")
20976                    || s.eq_ignore_ascii_case("conditional"))
20977            {
20978                self.advance();
20979            }
20980            if !matches!(self.peek(), Token::Ident(s)
20981                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20982            {
20983                return Err(self.err("expected WRAPPER after WITH".into()));
20984            }
20985            self.advance();
20986            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
20987            if matches!(self.peek(), Token::Ident(s)
20988                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
20989            {
20990                self.advance();
20991            }
20992            wrapper = true;
20993        }
20994        // ON EMPTY / ON ERROR clauses (two, in any order).
20995        let mut on_empty = JsonTableOnBehavior::Null;
20996        let mut on_error = JsonTableOnBehavior::Null;
20997        for _ in 0..2 {
20998            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
20999            {
21000                self.advance();
21001                Some(JsonTableOnBehavior::Error)
21002            } else if matches!(self.peek(), Token::Null) {
21003                self.advance();
21004                Some(JsonTableOnBehavior::Null)
21005            } else if matches!(self.peek(), Token::Default) {
21006                self.advance();
21007                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
21008            } else {
21009                None
21010            };
21011            let Some(behavior) = behavior else { break };
21012            // `ON {EMPTY|ERROR}`
21013            if !matches!(self.peek(), Token::On) {
21014                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
21015            }
21016            self.advance();
21017            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
21018                self.advance();
21019                on_empty = behavior;
21020            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
21021                self.advance();
21022                on_error = behavior;
21023            } else {
21024                return Err(self.err("expected EMPTY or ERROR after ON".into()));
21025            }
21026        }
21027        Ok(JsonTableColumn::Regular {
21028            name,
21029            ty,
21030            path,
21031            exists,
21032            format_json,
21033            wrapper,
21034            on_empty,
21035            on_error,
21036        })
21037    }
21038
21039    fn parse_optional_alias_with_columns(
21040        &mut self,
21041    ) -> Result<(Option<String>, Vec<String>), ParseError> {
21042        let alias = self.parse_optional_alias()?;
21043        if alias.is_none() {
21044            return Ok((None, Vec::new()));
21045        }
21046        let mut cols: Vec<String> = Vec::new();
21047        if matches!(self.peek(), Token::LParen) {
21048            self.advance();
21049            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21050                self.advance();
21051                cols.push(s);
21052                if matches!(self.peek(), Token::Comma) {
21053                    self.advance();
21054                    continue;
21055                }
21056                break;
21057            }
21058            if matches!(self.peek(), Token::RParen) {
21059                self.advance();
21060            }
21061        }
21062        Ok((alias, cols))
21063    }
21064
21065    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21066    /// whose keyword token was already consumed and whose `(` is the
21067    /// current token. Factored out of `parse_atom` (and marked
21068    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21069    /// recursive `parse_atom` frame — inlining them there enlarges the
21070    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21071    /// against, risking an overflow before the budget triggers.
21072    #[inline(never)]
21073    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21074        self.advance(); // (
21075        let mut args = Vec::new();
21076        if !matches!(self.peek(), Token::RParen) {
21077            loop {
21078                args.push(self.parse_expr(0)?);
21079                match self.peek() {
21080                    Token::Comma => {
21081                        self.advance();
21082                    }
21083                    Token::RParen => break,
21084                    other => {
21085                        return Err(self.err(alloc::format!(
21086                            "expected ',' or ')' in {name}() args, got {other:?}"
21087                        )));
21088                    }
21089                }
21090            }
21091        }
21092        self.advance(); // )
21093        Ok(Expr::FunctionCall {
21094            name: name.into(),
21095            args,
21096        })
21097    }
21098
21099    /// FROM-clause: a primary table reference plus zero-or-more joined
21100    /// peers expressed via either `, <table>` (cross-product, no ON) or
21101    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21102    /// v1.10 keeps the join list flat (left-associative nested-loop
21103    /// semantics).
21104    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21105        let primary = self.parse_table_ref()?;
21106        let primary_qual = primary
21107            .alias
21108            .clone()
21109            .unwrap_or_else(|| primary.name.clone());
21110        let joins = self.parse_from_joins(&primary_qual)?;
21111        Ok(FromClause { primary, joins })
21112    }
21113
21114    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21115    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21116    /// SAME grammar after its target table has already been consumed.
21117    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21118    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21119    /// be parsed forward, once.)
21120    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21121    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21122    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21123    /// desugaring, which needs a name for the left side of each equality.
21124    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21125        let mut joins = Vec::new();
21126        loop {
21127            // `, <table>` — cross-product with no ON.
21128            if matches!(self.peek(), Token::Comma) {
21129                self.advance();
21130                let table = self.parse_table_ref()?;
21131                joins.push(FromJoin {
21132                    kind: JoinKind::Cross,
21133                    table,
21134                    on: None,
21135                    using_cols: None,
21136                    natural: false,
21137                });
21138                continue;
21139            }
21140            // v7.37.16 — optional leading `NATURAL` before the join
21141            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21142            // not a lexer keyword (it arrives as a bare Ident), so match
21143            // it case-insensitively here. When present, no ON/USING
21144            // clause is allowed — the common columns are resolved at
21145            // execution time.
21146            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21147            if natural {
21148                self.advance();
21149            }
21150            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21151            // CROSS JOIN, and bare JOIN (defaults to INNER).
21152            let kind =
21153                match self.peek() {
21154                    Token::Inner => {
21155                        self.advance();
21156                        if !matches!(self.peek(), Token::Join) {
21157                            return Err(self
21158                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21159                        }
21160                        self.advance();
21161                        JoinKind::Inner
21162                    }
21163                    Token::Left => {
21164                        self.advance();
21165                        if matches!(self.peek(), Token::Outer) {
21166                            self.advance();
21167                        }
21168                        if !matches!(self.peek(), Token::Join) {
21169                            return Err(self.err(format!(
21170                                "expected JOIN after LEFT [OUTER], got {:?}",
21171                                self.peek()
21172                            )));
21173                        }
21174                        self.advance();
21175                        JoinKind::Left
21176                    }
21177                    Token::Cross => {
21178                        self.advance();
21179                        if !matches!(self.peek(), Token::Join) {
21180                            return Err(self
21181                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21182                        }
21183                        self.advance();
21184                        JoinKind::Cross
21185                    }
21186                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21187                    Token::Right => {
21188                        self.advance();
21189                        if matches!(self.peek(), Token::Outer) {
21190                            self.advance();
21191                        }
21192                        if !matches!(self.peek(), Token::Join) {
21193                            return Err(self.err(format!(
21194                                "expected JOIN after RIGHT [OUTER], got {:?}",
21195                                self.peek()
21196                            )));
21197                        }
21198                        self.advance();
21199                        JoinKind::Right
21200                    }
21201                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21202                    Token::Full => {
21203                        self.advance();
21204                        if matches!(self.peek(), Token::Outer) {
21205                            self.advance();
21206                        }
21207                        if !matches!(self.peek(), Token::Join) {
21208                            return Err(self.err(format!(
21209                                "expected JOIN after FULL [OUTER], got {:?}",
21210                                self.peek()
21211                            )));
21212                        }
21213                        self.advance();
21214                        JoinKind::FullOuter
21215                    }
21216                    Token::Join => {
21217                        self.advance();
21218                        JoinKind::Inner
21219                    }
21220                    _ => break,
21221                };
21222            let table = self.parse_table_ref()?;
21223            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21224            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21225            // where prev_table is the most-recent left-side table
21226            // (the previous join's table if any, else the FROM primary).
21227            // PG semantics around column merging are richer (USING'd
21228            // cols become deduplicated single output columns); for
21229            // sugar purposes the predicate-only form covers the
21230            // baseline corpus shape and chained `… JOIN x USING (k)
21231            // JOIN y USING (k)` calls.
21232            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21233            // common columns resolve at execution time.
21234            if natural {
21235                joins.push(FromJoin {
21236                    kind,
21237                    table,
21238                    on: None,
21239                    using_cols: None,
21240                    natural: true,
21241                });
21242                continue;
21243            }
21244            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21245            // v7.37.16 — capture the USING column list (in addition to
21246            // the ON desugar below) so the executor can perform PG's
21247            // column-merge on the output side.
21248            let mut using_cols: Option<Vec<String>> = None;
21249            let on = if matches!(self.peek(), Token::On) {
21250                self.advance();
21251                Some(self.parse_expr(0)?)
21252            } else if using_match {
21253                self.advance();
21254                if !matches!(self.peek(), Token::LParen) {
21255                    return Err(
21256                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21257                    );
21258                }
21259                self.advance();
21260                let mut cols: Vec<String> = Vec::new();
21261                loop {
21262                    match self.peek().clone() {
21263                        Token::Ident(s) | Token::QuotedIdent(s) => {
21264                            self.advance();
21265                            cols.push(s);
21266                        }
21267                        other => {
21268                            return Err(self.err(format!(
21269                                "expected column name inside USING (…), got {other:?}"
21270                            )));
21271                        }
21272                    }
21273                    match self.peek() {
21274                        Token::Comma => {
21275                            self.advance();
21276                            continue;
21277                        }
21278                        Token::RParen => {
21279                            self.advance();
21280                            break;
21281                        }
21282                        other => {
21283                            return Err(self.err(format!(
21284                                "expected ',' or ')' inside USING (…), got {other:?}"
21285                            )));
21286                        }
21287                    }
21288                }
21289                if cols.is_empty() {
21290                    return Err(self.err("USING (…) requires at least one column".to_string()));
21291                }
21292                using_cols = Some(cols.clone());
21293                // Pick the left-side alias: prev join's table if any,
21294                // else FROM primary. Use alias when present, else
21295                // table name (PG-equivalent qualifier).
21296                let left_qual: String = joins
21297                    .last()
21298                    .map(|j| {
21299                        j.table
21300                            .alias
21301                            .clone()
21302                            .unwrap_or_else(|| j.table.name.clone())
21303                    })
21304                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21305                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21306                let mut iter = cols.into_iter().map(|c| Expr::Binary {
21307                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21308                        qualifier: Some(left_qual.clone()),
21309                        name: c.clone(),
21310                    })),
21311                    op: crate::ast::BinOp::Eq,
21312                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21313                        qualifier: Some(right_qual.clone()),
21314                        name: c,
21315                    })),
21316                });
21317                let first = iter.next().expect("at least one col");
21318                Some(iter.fold(first, |acc, pred| Expr::Binary {
21319                    lhs: alloc::boxed::Box::new(acc),
21320                    op: crate::ast::BinOp::And,
21321                    rhs: alloc::boxed::Box::new(pred),
21322                }))
21323            } else if kind == JoinKind::Cross {
21324                None
21325            } else {
21326                return Err(self.err(format!(
21327                    "expected ON or USING after {:?} JOIN, got {:?}",
21328                    kind,
21329                    self.peek()
21330                )));
21331            };
21332            joins.push(FromJoin {
21333                kind,
21334                table,
21335                on,
21336                using_cols,
21337                natural: false,
21338            });
21339        }
21340        Ok(joins)
21341    }
21342
21343    /// Optional alias after an expression or table:
21344    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21345    /// accepted (PG-style implicit alias). Returns `None` if the next token
21346    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21347    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21348        if matches!(self.peek(), Token::As) {
21349            self.advance();
21350            // v7.39 (round 340, V56) — after AS the next token MUST be an
21351            // identifier. This used to return None and "let the caller
21352            // surface the error on the next expectation", but when AS is
21353            // the LAST token there is no next expectation: `SELECT 1 AS`
21354            // parsed clean and silently dropped the alias. PG rejects it.
21355            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21356                return self.expect_ident_like().map(Some);
21357            }
21358            return Err(self.err(alloc::format!(
21359                "expected an alias after AS, got {:?}",
21360                self.peek()
21361            )));
21362        }
21363        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21364        // grammar reserves a long list of follow-keywords from the
21365        // alias slot. SPG's bareword approximation: skip a small
21366        // set of idents that would otherwise be swallowed as the
21367        // table alias and break trailing clauses like CREATE
21368        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21369        // CONFLICT WHERE shapes.
21370        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21371            if is_alias_stopword(s) {
21372                return Ok(None);
21373            }
21374            return Ok(self.expect_ident_like().ok());
21375        }
21376        Ok(None)
21377    }
21378
21379    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21380    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21381        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21382        // error beats a stack overflow (an overflow aborts the
21383        // embedding host process).
21384        self.enter_nested()?;
21385        let r = self.parse_expr_inner(min_prec);
21386        self.nest_depth -= 1;
21387        r
21388    }
21389
21390    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21391    /// When the upcoming tokens form one, return the underlying
21392    /// operator token and the position just past the closing paren
21393    /// so the binary loop can dispatch on the plain operator.
21394    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21395        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21396            return None;
21397        }
21398        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21399            return None;
21400        }
21401        let mut i = self.pos + 2;
21402        // Optional schema qualifier (pg_catalog.<op> etc.).
21403        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21404            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21405        {
21406            i += 2;
21407        }
21408        let op_tok = self.tokens.get(i)?.clone();
21409        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21410            return None;
21411        }
21412        Some((i + 2, op_tok))
21413    }
21414
21415    /// PG operator symbols that lower onto function calls in
21416    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21417    /// family → regexp_like, comparison rung), `^@` (starts_with,
21418    /// comparison rung), `^` (power, tighter than `*`), `#`
21419    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21420    /// subset of the OR bits so the subtraction never borrows).
21421    fn try_symbol_operator(
21422        &mut self,
21423        lhs: &Expr,
21424        min_prec: u8,
21425    ) -> Result<Option<Expr>, ParseError> {
21426        enum Sym {
21427            Regex { ci: bool, negated: bool },
21428            Like { ci: bool, negated: bool },
21429            StartsWith,
21430            Power,
21431            Xor,
21432            RangeAdjacent,
21433        }
21434        // v7.39 (IS-precedence knife) — the low-precedence postfix
21435        // predicates ride this existing leaf call (zero new frame slots
21436        // on the nesting chain).
21437        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21438            return Ok(Some(e));
21439        }
21440        let (sym, prec): (Sym, u8) = match self.peek() {
21441            Token::Tilde => (
21442                Sym::Regex {
21443                    ci: false,
21444                    negated: false,
21445                },
21446                5,
21447            ),
21448            Token::TildeStar => (
21449                Sym::Regex {
21450                    ci: true,
21451                    negated: false,
21452                },
21453                5,
21454            ),
21455            Token::NotTilde => (
21456                Sym::Regex {
21457                    ci: false,
21458                    negated: true,
21459                },
21460                5,
21461            ),
21462            Token::NotTildeStar => (
21463                Sym::Regex {
21464                    ci: true,
21465                    negated: true,
21466                },
21467                5,
21468            ),
21469            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21470            Token::DoubleTilde => (
21471                Sym::Like {
21472                    ci: false,
21473                    negated: false,
21474                },
21475                5,
21476            ),
21477            Token::DoubleTildeStar => (
21478                Sym::Like {
21479                    ci: true,
21480                    negated: false,
21481                },
21482                5,
21483            ),
21484            Token::NotDoubleTilde => (
21485                Sym::Like {
21486                    ci: false,
21487                    negated: true,
21488                },
21489                5,
21490            ),
21491            Token::NotDoubleTildeStar => (
21492                Sym::Like {
21493                    ci: true,
21494                    negated: true,
21495                },
21496                5,
21497            ),
21498            Token::CaretAt => (Sym::StartsWith, 5),
21499            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21500            // tighter than `* / & |`, which the prec-9 rung preserves —
21501            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21502            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21503            Token::Caret => (Sym::Power, 9),
21504            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21505            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21506            Token::Hash => (Sym::Xor, 6),
21507            Token::Adjacent => (Sym::RangeAdjacent, 5),
21508            _ => return Ok(None),
21509        };
21510        if prec < min_prec {
21511            return Ok(None);
21512        }
21513        self.advance();
21514        let rhs = self.parse_expr(prec + 1)?;
21515        let out = match sym {
21516            Sym::Regex { ci, negated } => {
21517                let mut args = alloc::vec![lhs.clone(), rhs];
21518                if ci {
21519                    args.push(Expr::Literal(Literal::String(String::from("i"))));
21520                }
21521                maybe_not(
21522                    Expr::FunctionCall {
21523                        name: String::from("regexp_like"),
21524                        args,
21525                    },
21526                    negated,
21527                )
21528            }
21529            Sym::Like { ci, negated } => Expr::Like {
21530                expr: alloc::boxed::Box::new(lhs.clone()),
21531                pattern: alloc::boxed::Box::new(rhs),
21532                negated,
21533                case_insensitive: ci,
21534            },
21535            Sym::StartsWith => Expr::FunctionCall {
21536                name: String::from("starts_with"),
21537                args: alloc::vec![lhs.clone(), rhs],
21538            },
21539            Sym::Power => Expr::FunctionCall {
21540                name: String::from("power"),
21541                args: alloc::vec![lhs.clone(), rhs],
21542            },
21543            // `#` bitwise XOR — a real operator now (was desugared to
21544            // `(a|b)-(a&b)`, algebraically identical for integers but
21545            // undefined for bit strings; the direct op handles both).
21546            Sym::Xor => Expr::Binary {
21547                lhs: Box::new(lhs.clone()),
21548                op: BinOp::BitXor,
21549                rhs: Box::new(rhs),
21550            },
21551            // range `-|-` "is adjacent to" — lowered to a catalog function.
21552            Sym::RangeAdjacent => Expr::FunctionCall {
21553                name: String::from("range_adjacent"),
21554                args: alloc::vec![lhs.clone(), rhs],
21555            },
21556        };
21557        Ok(Some(out))
21558    }
21559
21560    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21561    /// predicates, moved out of the tight postfix-cast loop: PG binds
21562    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21563    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21564    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21565    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21566    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21567    /// when nothing at this position belongs to the family. Out-of-line
21568    /// (`inline(never)`): the caller sits on the per-nesting-level frame
21569    /// chain that MAX_NEST_DEPTH is tuned against.
21570    #[inline(never)]
21571    fn parse_postfix_predicate(
21572        &mut self,
21573        lhs: &Expr,
21574        min_prec: u8,
21575    ) -> Result<Option<Expr>, ParseError> {
21576        // Reached through try_symbol_operator (an existing leaf call of
21577        // the binary loop) so NO new stack slots land on the per-nesting
21578        // frame chain; the lhs clones only when a predicate actually
21579        // consumes it.
21580        match self.peek() {
21581            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21582            // comparison family rung 5 (each +1 from the pre-XOR ladder).
21583            Token::Is if min_prec <= 4 => {}
21584            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21585            Token::Not
21586                if min_prec <= 5
21587                    && matches!(
21588                        self.tokens.get(self.pos + 1),
21589                        Some(Token::Between | Token::In | Token::Like)
21590                    ) => {}
21591            Token::Not | Token::Ident(_)
21592                if min_prec <= 5
21593                    && (matches!(self.peek(), Token::Ident(s)
21594                            if s.eq_ignore_ascii_case("ilike")
21595                                || (self.mysql_dialect
21596                                    && (s.eq_ignore_ascii_case("regexp")
21597                                        || s.eq_ignore_ascii_case("rlike")))
21598                                || (s.eq_ignore_ascii_case("similar")
21599                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21600                        || (matches!(self.peek(), Token::Not)
21601                            && matches!(self.tokens.get(self.pos + 1),
21602                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21603                                    || (self.mysql_dialect
21604                                        && (s.eq_ignore_ascii_case("regexp")
21605                                            || s.eq_ignore_ascii_case("rlike")))
21606                                    || s.eq_ignore_ascii_case("similar")))) => {}
21607            _ => return Ok(None),
21608        }
21609        let mut expr = lhs.clone();
21610        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21611        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21612        if min_prec <= 4 {
21613            if matches!(self.peek(), Token::Is) {
21614                self.advance();
21615                let negated = if matches!(self.peek(), Token::Not) {
21616                    self.advance();
21617                    true
21618                } else {
21619                    false
21620                };
21621                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21622                // mailrs pg_dump.
21623                if matches!(self.peek(), Token::Distinct) {
21624                    self.advance();
21625                    if !matches!(self.peek(), Token::From) {
21626                        return Err(self.err(format!(
21627                            "expected FROM after IS{} DISTINCT, got {:?}",
21628                            if negated { " NOT" } else { "" },
21629                            self.peek()
21630                        )));
21631                    }
21632                    self.advance();
21633                    // Right-hand side: parse at the same precedence
21634                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21635                    // groups as `x IS DISTINCT FROM (a + b)`.
21636                    let rhs = self.parse_expr(5)?;
21637                    let op = if negated {
21638                        BinOp::IsNotDistinctFrom
21639                    } else {
21640                        BinOp::IsDistinctFrom
21641                    };
21642                    expr = Expr::Binary {
21643                        op,
21644                        lhs: Box::new(expr),
21645                        rhs: Box::new(rhs),
21646                    };
21647                    {
21648                        return Ok(Some(expr));
21649                    }
21650                }
21651                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
21652                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
21653                // Lowers onto pg_is_json(x, kind); NOT wraps the
21654                // call in a logical negation.
21655                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21656                if s.eq_ignore_ascii_case("json"))
21657                {
21658                    self.advance(); // JSON
21659                    let kind = match self.peek() {
21660                        Token::Ident(s) | Token::QuotedIdent(s)
21661                            if matches!(
21662                                s.to_ascii_lowercase().as_str(),
21663                                "value" | "object" | "array" | "scalar"
21664                            ) =>
21665                        {
21666                            let k = s.to_ascii_lowercase();
21667                            self.advance();
21668                            k
21669                        }
21670                        _ => "value".to_string(),
21671                    };
21672                    let call = Expr::FunctionCall {
21673                        name: "pg_is_json".to_string(),
21674                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
21675                    };
21676                    expr = if negated {
21677                        Expr::Unary {
21678                            op: UnOp::Not,
21679                            expr: Box::new(call),
21680                        }
21681                    } else {
21682                        call
21683                    };
21684                    {
21685                        return Ok(Some(expr));
21686                    }
21687                }
21688                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
21689                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
21690                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
21691                {
21692                    let form_kw = match self.peek() {
21693                        Token::Ident(s) | Token::QuotedIdent(s)
21694                            if matches!(
21695                                s.to_ascii_uppercase().as_str(),
21696                                "NFC" | "NFD" | "NFKC" | "NFKD"
21697                            ) && matches!(
21698                                self.tokens.get(self.pos + 1),
21699                                Some(Token::Ident(n) | Token::QuotedIdent(n))
21700                                    if n.eq_ignore_ascii_case("normalized")
21701                            ) =>
21702                        {
21703                            Some(s.to_ascii_uppercase())
21704                        }
21705                        _ => None,
21706                    };
21707                    let bare_normalized = form_kw.is_none()
21708                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
21709                        if s.eq_ignore_ascii_case("normalized"));
21710                    if form_kw.is_some() || bare_normalized {
21711                        if form_kw.is_some() {
21712                            self.advance(); // form keyword
21713                        }
21714                        self.advance(); // NORMALIZED
21715                        let mut args = alloc::vec![expr];
21716                        if let Some(f) = form_kw {
21717                            args.push(Expr::Literal(Literal::String(f)));
21718                        }
21719                        let call = Expr::FunctionCall {
21720                            name: "is_normalized".to_string(),
21721                            args,
21722                        };
21723                        expr = if negated {
21724                            Expr::Unary {
21725                                op: UnOp::Not,
21726                                expr: Box::new(call),
21727                            }
21728                        } else {
21729                            call
21730                        };
21731                        {
21732                            return Ok(Some(expr));
21733                        }
21734                    }
21735                }
21736                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
21737                // three-valued boolean tests. IS TRUE/FALSE never
21738                // return NULL, so they lower to CASE forms whose
21739                // ELSE catches the NULL branch; IS UNKNOWN on a
21740                // boolean is exactly IS NULL.
21741                if matches!(self.peek(), Token::True | Token::False)
21742                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
21743                {
21744                    let tok = self.advance();
21745                    let test = match tok {
21746                        Token::True => Some(true),
21747                        Token::False => Some(false),
21748                        _ => None, // UNKNOWN
21749                    };
21750                    // v7.39 (round 328, V45) — kept as what the user
21751                    // wrote. These used to be lowered here into `CASE` /
21752                    // `IS NULL`; the semantics were right but the AST no
21753                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
21754                    // was echoed back as
21755                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
21756                    expr = Expr::BoolTest {
21757                        expr: Box::new(expr),
21758                        value: test,
21759                        negated,
21760                    };
21761                    {
21762                        return Ok(Some(expr));
21763                    }
21764                }
21765                if !matches!(self.peek(), Token::Null) {
21766                    return Err(self.err(format!(
21767                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
21768                    if negated { " NOT" } else { "" },
21769                    self.peek()
21770                )));
21771                }
21772                self.advance();
21773                expr = Expr::IsNull {
21774                    expr: Box::new(expr),
21775                    negated,
21776                };
21777                {
21778                    return Ok(Some(expr));
21779                }
21780            }
21781        }
21782        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
21783        if min_prec <= 5 {
21784            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
21785            // Look one token ahead so a stray `NOT` not followed by any of
21786            // these flows through to the early return below untouched.
21787            let negated = if matches!(self.peek(), Token::Not) {
21788                let next = self.tokens.get(self.pos + 1);
21789                matches!(next, Some(Token::Between | Token::In | Token::Like))
21790                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21791                    || (self.mysql_dialect
21792                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
21793                    || s.eq_ignore_ascii_case("similar"))
21794            } else {
21795                false
21796            };
21797            if negated {
21798                self.advance();
21799            }
21800            if matches!(self.peek(), Token::Between) {
21801                expr = self.parse_between_tail(expr, negated)?;
21802                {
21803                    return Ok(Some(expr));
21804                }
21805            }
21806            if matches!(self.peek(), Token::In) {
21807                if self.suppress_in_tail && !negated {
21808                    // POSITION(sub IN str) — IN belongs to the
21809                    // enclosing function syntax; stop here.
21810                    {
21811                        return Ok(None);
21812                    }
21813                }
21814                expr = self.parse_in_tail(expr, negated)?;
21815                {
21816                    return Ok(Some(expr));
21817                }
21818            }
21819            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
21820            // lowers onto the internal __similar_to(expr, pat[, esc]) call
21821            // (the SQL→regex transform runs inside, in the backtracking-
21822            // friendly shape SPG's matcher needs).
21823            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
21824                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
21825            {
21826                self.advance(); // SIMILAR
21827                self.advance(); // TO
21828                let pattern = self.parse_expr(6)?;
21829                let mut args = alloc::vec![expr, pattern];
21830                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21831                    self.advance();
21832                    args.push(self.parse_expr(6)?);
21833                }
21834                let call = Expr::FunctionCall {
21835                    name: "__similar_to".to_string(),
21836                    args,
21837                };
21838                expr = maybe_not(call, negated);
21839                {
21840                    return Ok(Some(expr));
21841                }
21842            }
21843            if matches!(self.peek(), Token::Like) {
21844                self.advance();
21845                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
21846                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
21847                    expr = q;
21848                    {
21849                        return Ok(Some(expr));
21850                    }
21851                }
21852                // Pattern at the same precedence as other comparison RHSes —
21853                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
21854                let mut pattern = self.parse_expr(6)?;
21855                // `ESCAPE 'c'` — rewrite a literal pattern to the
21856                // default backslash escape at parse time. Custom
21857                // escapes on non-literal patterns would need
21858                // matcher support; error honestly.
21859                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
21860                    self.advance();
21861                    let esc = self.parse_expr(6)?;
21862                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
21863                }
21864                expr = Expr::Like {
21865                    expr: Box::new(expr),
21866                    pattern: Box::new(pattern),
21867                    negated,
21868                    case_insensitive: false,
21869                };
21870                {
21871                    return Ok(Some(expr));
21872                }
21873            }
21874            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
21875            // keyword reaches us as a plain identifier.
21876            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
21877                self.advance();
21878                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
21879                    expr = q;
21880                    {
21881                        return Ok(Some(expr));
21882                    }
21883                }
21884                let pattern = self.parse_expr(6)?;
21885                expr = Expr::Like {
21886                    expr: Box::new(expr),
21887                    pattern: Box::new(pattern),
21888                    negated,
21889                    case_insensitive: true,
21890                };
21891                {
21892                    return Ok(Some(expr));
21893                }
21894            }
21895            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
21896            // operator (RLIKE is the alias). It is a keyword, not `~`, and
21897            // matches case-insensitively under the default collation, so it
21898            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
21899            // `~*` operator uses, wrapped in NOT when negated.
21900            if self.mysql_dialect
21901                && matches!(self.peek(), Token::Ident(s)
21902                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
21903            {
21904                self.advance();
21905                let pattern = self.parse_expr(6)?;
21906                let call = Expr::FunctionCall {
21907                    name: String::from("regexp_like"),
21908                    args: alloc::vec![
21909                        expr,
21910                        pattern,
21911                        Expr::Literal(Literal::String(String::from("i"))),
21912                    ],
21913                };
21914                return Ok(Some(maybe_not(call, negated)));
21915            }
21916        }
21917        let _ = expr;
21918        Ok(None)
21919    }
21920
21921    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21922        let mut lhs = self.parse_unary()?;
21923        let mut chain_len = 0usize;
21924        loop {
21925            // OPERATOR([schema.]op) reduces to its underlying
21926            // operator token before the normal dispatch.
21927            let explicit = self.peek_explicit_operator();
21928            let dispatch = match &explicit {
21929                Some((_, tok)) => self.binop_here(tok),
21930                None => self.binop_here(self.peek()),
21931            };
21932            let Some((op, prec)) = dispatch else {
21933                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
21934                // of the symbol family. `binop_here` answers None for them
21935                // because they lower onto function calls rather than a
21936                // BinOp, and the fallback below reads `self.peek()` — the
21937                // word OPERATOR, not the operator. `pg_dump` writes every
21938                // catalog predicate this way, so its first query failed
21939                // and no dump ran:
21940                //
21941                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
21942                //
21943                // Collapsing the wrapper to the operator it names puts the
21944                // token where the fallback already looks.
21945                if let Some((next, op_tok)) = explicit {
21946                    self.tokens.splice(self.pos..next, [op_tok]);
21947                }
21948                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
21949                    lhs = e;
21950                    chain_len += 1;
21951                    if chain_len > MAX_BINARY_CHAIN {
21952                        return Err(self.err(alloc::format!(
21953                            "more than {MAX_BINARY_CHAIN} chained binary operators"
21954                        )));
21955                    }
21956                    continue;
21957                }
21958                break;
21959            };
21960            if prec < min_prec {
21961                break;
21962            }
21963            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
21964            // iteratively but evaluates and drops recursively;
21965            // depth beyond the budget overflows worker stacks.
21966            chain_len += 1;
21967            if chain_len > MAX_BINARY_CHAIN {
21968                return Err(self.err(alloc::format!(
21969                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
21970                )));
21971            }
21972            match explicit {
21973                Some((end_pos, _)) => self.pos = end_pos,
21974                None => {
21975                    self.advance();
21976                }
21977            }
21978            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
21979            // ANY is a bare ident; ALL is a reserved Token. Both
21980            // require an immediate `(` to disambiguate from
21981            // identifier columns named `any` / `all`.
21982            let any_kind = match self.peek() {
21983                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
21984                    Some(false)
21985                }
21986                Token::Ident(s) | Token::QuotedIdent(s)
21987                    if (s.eq_ignore_ascii_case("any")
21988                        || s.eq_ignore_ascii_case("some")
21989                        || s.eq_ignore_ascii_case("all"))
21990                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
21991                {
21992                    Some(!s.eq_ignore_ascii_case("all"))
21993                }
21994                _ => None,
21995            };
21996            if let Some(is_any) = any_kind {
21997                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
21998                continue;
21999            }
22000            let rhs = self.parse_expr(prec + 1)?;
22001            lhs = Expr::Binary {
22002                lhs: Box::new(lhs),
22003                op,
22004                rhs: Box::new(rhs),
22005            };
22006        }
22007        Ok(lhs)
22008    }
22009
22010    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
22011    /// and the array form.
22012    ///
22013    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
22014    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
22015    /// this block's `Expr` temporaries and four `format!` sites slots in
22016    /// that frame on every level of `((((1))))`, which never reaches it.
22017    #[inline(never)]
22018    fn parse_any_all_rhs(
22019        &mut self,
22020        lhs: Expr,
22021        op: BinOp,
22022        is_any: bool,
22023    ) -> Result<Expr, ParseError> {
22024        self.advance(); // ident
22025        self.advance(); // (
22026        // `x op ANY (SELECT …)` — the quantified-subquery
22027        // form. `= ANY` is exactly IN; the other operators
22028        // lower onto EXISTS over the subquery as a derived
22029        // table, comparing against its single projection
22030        // aliased __v (x's columns resolve correlated).
22031        // ALL is the negated-EXISTS complement; a NULL
22032        // element makes PG return NULL where this lowering
22033        // returns true — the NOT NULL column case (the
22034        // practical one) is exact.
22035        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
22036            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22037            // legal PG too (round-151 sibling). Out-of-line
22038            // (#[inline(never)] helper) — this sits on
22039            // parse_expr's recursive frame and the two-armed
22040            // SELECT temporary blew the nesting-budget stack.
22041            let mut sub = self.parse_any_all_select_body()?;
22042            if !matches!(self.peek(), Token::RParen) {
22043                return Err(self.err(alloc::format!(
22044                    "expected ')' after ANY/ALL subquery, got {:?}",
22045                    self.peek()
22046                )));
22047            }
22048            self.advance();
22049            if sub.items.len() != 1 {
22050                return Err(self.err(alloc::format!(
22051                    "ANY/ALL subquery must return one column, got {}",
22052                    sub.items.len()
22053                )));
22054            }
22055            if is_any && matches!(op, BinOp::Eq) {
22056                return Ok(Expr::InSubquery {
22057                    expr: Box::new(lhs),
22058                    subquery: Box::new(sub),
22059                    negated: false,
22060                });
22061            }
22062            // The engine's subquery resolvers materialise
22063            // the single-column result into an ARRAY the
22064            // existing AnyAll three-valued eval consumes.
22065            return Ok(Expr::AnyAll {
22066                expr: Box::new(lhs),
22067                op,
22068                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22069                is_any,
22070            });
22071        }
22072        let arr = self.parse_expr(0)?;
22073        if !matches!(self.peek(), Token::RParen) {
22074            return Err(self.err(alloc::format!(
22075                "expected ')' after ANY/ALL argument, got {:?}",
22076                self.peek()
22077            )));
22078        }
22079        self.advance();
22080        Ok(Expr::AnyAll {
22081            expr: Box::new(lhs),
22082            op,
22083            array: Box::new(arr),
22084            is_any,
22085        })
22086    }
22087
22088    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22089    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22090    #[inline(never)]
22091    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22092        self.advance();
22093        let e = self.parse_expr(9)?;
22094        Ok(build_center_call(e))
22095    }
22096
22097    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22098    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22099    /// unary minus.
22100    ///
22101    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22102    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22103    /// the Expr-sized local stays out of that frame.
22104    #[inline(never)]
22105    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22106        self.advance();
22107        let e = self.parse_expr(9)?;
22108        Ok(Expr::FunctionCall {
22109            name: alloc::string::String::from(name),
22110            args: alloc::vec![e],
22111        })
22112    }
22113
22114    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22115    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22116    #[inline(never)]
22117    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22118        self.advance();
22119        let e = self.parse_expr(9)?;
22120        Ok(Expr::FunctionCall {
22121            name: alloc::string::String::from(if vertical {
22122                "isvertical"
22123            } else {
22124                "ishorizontal"
22125            }),
22126            args: alloc::vec![e],
22127        })
22128    }
22129
22130    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22131    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22132    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22133    #[inline(never)]
22134    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22135        self.advance();
22136        let e = self.parse_expr(9)?;
22137        Ok(Expr::Cast {
22138            expr: Box::new(e),
22139            target: CastTarget::Named("binary".to_string()),
22140        })
22141    }
22142
22143    /// The prefix operators that share one shape: take the token, parse
22144    /// an operand at `prec`, wrap it.
22145    ///
22146    /// `#[inline(never)]`, and one function instead of five arms, for the
22147    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22148    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22149    /// debug build gives EVERY arm's locals a slot in the frame, whichever
22150    /// arm runs. `((((1))))` reaches none of these arms and was carrying
22151    /// five `Expr`-sized locals per level for them anyway.
22152    #[inline(never)]
22153    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22154        self.advance();
22155        let e = self.parse_expr(prec)?;
22156        Ok(Expr::Unary {
22157            op,
22158            expr: Box::new(e),
22159        })
22160    }
22161
22162    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22163    /// and separate from it because of the literal folding below and the
22164    /// `format!` temporaries that folding needs.
22165    #[inline(never)]
22166    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22167        self.advance();
22168        // v7.39 (round 549) — fold the sign into an integer literal that
22169        // only fits once it is negative.
22170        //
22171        // `9223372036854775808` is one past i64::MAX, so the lexer hands
22172        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22173        // folds the sign first, so `-9223372036854775808` is a bigint
22174        // there — and `-9223372036854775808 - 1` raises "bigint out of
22175        // range" where SPG quietly answered -9223372036854775809, a value
22176        // no bigint can hold. The arithmetic itself was already checked;
22177        // only the literal's type was wrong.
22178        if let Token::Numeric(lit) = self.peek()
22179            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22180        {
22181            self.advance();
22182            return Ok(Expr::Literal(Literal::Integer(folded)));
22183        }
22184        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22185        // `<->` slotted into 5 and arithmetic shifted up).
22186        let e = self.parse_expr(9)?;
22187        Ok(Expr::Unary {
22188            op: UnOp::Neg,
22189            expr: Box::new(e),
22190        })
22191    }
22192
22193    /// tsquery `!!` prefix negation, lowered to the catalog function.
22194    /// Binds like unary minus. Out-of-line for the frame reason on
22195    /// `parse_unary_op`.
22196    #[inline(never)]
22197    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22198        self.advance();
22199        let e = self.parse_expr(9)?;
22200        Ok(Expr::FunctionCall {
22201            name: String::from("tsquery_not"),
22202            args: alloc::vec![e],
22203        })
22204    }
22205
22206    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22207        match self.peek() {
22208            // NOT binds tighter than AND / XOR / OR but looser than
22209            // comparisons — its operand takes everything ≥ the comparison
22210            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22211            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22212            // was rung 3, behaviour-identical when 3 was unused; AND now
22213            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22214            Token::Not => self.parse_unary_op(UnOp::Not, 4),
22215            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22216            // The body is out-of-line: `parse_unary` is one of the three
22217            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22218            // inline arm here overflowed the native stack in
22219            // `nesting_budget_errors_cleanly` — the guard test caught it,
22220            // exactly as the eval-side cliff did in rounds 346 and 351.
22221            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22222                self.parse_binary_prefix()
22223            }
22224            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22225            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22226            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22227            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22228            Token::Minus => self.parse_prefix_minus(),
22229            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22230            // worked only because the lexer reads it as one signed literal;
22231            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22232            // PG18 and MariaDB take all of them. Binds like unary minus.
22233            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22234            // Bitwise NOT binds like unary minus.
22235            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22236            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22237            // "center of" operator; desugars to center(x). The whole arm
22238            // is out-of-line: parse_unary sits on the per-nesting-level
22239            // frame chain that MAX_NEST_DEPTH is tuned against, so no
22240            // Expr-sized local may live in this frame.
22241            Token::TsMatch => self.parse_prefix_center(),
22242            // v7.39 (round 508) — the prefix operators that are named
22243            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22244            // is length. Out-of-line for the same nesting-frame reason as
22245            // parse_prefix_center — parse_unary sits on the recursive cycle
22246            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22247            // live in this frame.
22248            Token::At => self.parse_prefix_call("abs"),
22249            Token::Hash => self.parse_prefix_call("npoints"),
22250            Token::AtMinusAt => self.parse_prefix_call("length"),
22251            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22252            // "is horizontal" (lseg / line); desugars to the existing
22253            // isvertical()/ishorizontal() functions. Out-of-line for the
22254            // same nesting-frame reason as parse_prefix_center.
22255            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22256            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22257            Token::DoubleBang => self.parse_prefix_tsquery_not(),
22258            _ => self.parse_atom(),
22259        }
22260    }
22261
22262    /// Parse a parenthesised scalar subquery body after the caller has consumed
22263    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22264    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22265    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22266    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22267    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22268    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22269    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22270    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22271    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22272    /// tips the deep-nesting test into a stack overflow).
22273    #[inline(never)]
22274    fn array_subquery_ahead(&self) -> bool {
22275        if !matches!(self.peek(), Token::LParen) {
22276            return false;
22277        }
22278        matches!(
22279            self.tokens.get(self.pos + 1),
22280            Some(Token::Select | Token::Values)
22281        ) || matches!(
22282            self.tokens.get(self.pos + 1),
22283            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22284        )
22285    }
22286
22287    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22288    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22289    /// locals stay off parse_atom's recursive frame (round 105).
22290    #[inline(never)]
22291    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22292        self.advance(); // consume `[`
22293        let mut items: Vec<Expr> = Vec::new();
22294        if !matches!(self.peek(), Token::RBracket) {
22295            loop {
22296                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22297                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22298                if matches!(self.peek(), Token::LBracket) {
22299                    items.push(self.parse_array_bracket_body()?);
22300                } else {
22301                    items.push(self.parse_expr(0)?);
22302                }
22303                match self.peek() {
22304                    Token::Comma => {
22305                        self.advance();
22306                    }
22307                    Token::RBracket => break,
22308                    other => {
22309                        return Err(self.err(alloc::format!(
22310                            "expected ',' or ']' in ARRAY literal, got {other:?}"
22311                        )));
22312                    }
22313                }
22314            }
22315        }
22316        self.advance(); // consume `]`
22317        Ok(Expr::Array(items))
22318    }
22319
22320    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22321    /// is already consumed; the current token is `(`. Desugars to a scalar
22322    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22323    /// the subquery's single-column rows in order — reusing the existing
22324    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22325    /// keeps the large `Statement` local off parse_atom's recursive frame.
22326    #[inline(never)]
22327    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22328        self.advance(); // consume `(`
22329        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22330            if w.eq_ignore_ascii_case("with"));
22331        let sub = if is_with {
22332            self.advance(); // WITH
22333            self.parse_with_cte_then_select()?
22334        } else {
22335            self.parse_select_stmt()?
22336        };
22337        if !matches!(self.peek(), Token::RParen) {
22338            return Err(self.err(alloc::format!(
22339                "expected ')' to close ARRAY(subquery), got {:?}",
22340                self.peek()
22341            )));
22342        }
22343        self.advance(); // consume `)`
22344        // Reuse the parser to build the array_agg wrapper from the subquery's
22345        // canonical text — avoids hand-constructing the derived-table AST.
22346        let wrapper = alloc::format!(
22347            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22348        );
22349        let stmt = parse_statement(&wrapper)
22350            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22351        let Statement::Select(sel) = stmt else {
22352            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22353        };
22354        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22355    }
22356
22357    #[inline(never)]
22358    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22359        let inner = if is_with {
22360            self.advance(); // WITH
22361            self.parse_with_cte_then_select()?
22362        } else {
22363            self.parse_select_stmt()?
22364        };
22365        match self.advance() {
22366            Token::RParen => {
22367                let Statement::Select(s) = inner else {
22368                    return Err(ParseError {
22369                        message: "scalar subquery body must be a SELECT".into(),
22370                        token_pos: self.consumed_pos(),
22371                    });
22372                };
22373                Ok(Expr::ScalarSubquery(Box::new(s)))
22374            }
22375            other => Err(ParseError {
22376                message: format!("expected ')' after scalar subquery, got {other:?}"),
22377                token_pos: self.consumed_pos(),
22378            }),
22379        }
22380    }
22381
22382    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22383    /// literals. The lexer splits them into an ident + string; recombine
22384    /// here. Out-of-line and returning `Option` so `parse_atom` — the
22385    /// recursive frame the 768 KiB stack budget is tuned against — pays no
22386    /// frame for the `body` / `bits` strings and their char loops (the
22387    /// round-367 frame cliff, M20).
22388    #[inline(never)]
22389    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22390        let is_hex = match self.peek() {
22391            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22392            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22393            _ => return None,
22394        };
22395        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22396            return None;
22397        }
22398        // v7.39.3 — where the LITERAL starts, because the errors below
22399        // are about the literal and both engines point at it. `err`
22400        // reports the CURRENT token, which by then is the one after the
22401        // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22402        // `near '…'` snippet — which runs from the reported position to
22403        // the end — came out empty where MySQL 9.7.2 says `near
22404        // 'x'123''`.
22405        let lit_pos = self.pos;
22406        self.advance();
22407        let Token::String(body) = self.advance() else {
22408            unreachable!("guarded above");
22409        };
22410        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22411        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22412        // (hex pairs, even count required — MariaDB errors on an odd
22413        // count); `b'1010'` packs its bits big-endian, left-padded to a
22414        // byte. Lower both onto the bytea cast.
22415        if self.mysql_dialect {
22416            if is_hex {
22417                if body.len() % 2 == 1 {
22418                    return Some(Err(self.err_at(
22419                        lit_pos,
22420                        alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22421                    )));
22422                }
22423                for c in body.chars() {
22424                    if !c.is_ascii_hexdigit() {
22425                        return Some(Err(self.err_at(
22426                            lit_pos,
22427                            alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22428                        )));
22429                    }
22430                }
22431                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22432            }
22433            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22434                return Some(Err(self.err_at(
22435                    lit_pos,
22436                    alloc::format!("invalid binary digit {bad:?} in b'…'"),
22437                )));
22438            }
22439            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22440        }
22441        let bits = if is_hex {
22442            let mut out = String::with_capacity(body.len() * 4);
22443            for c in body.chars() {
22444                let Some(d) = c.to_digit(16) else {
22445                    // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22446                    // own quoting: `"g" is not a valid hexadecimal
22447                    // digit` (measured, with the caret on the literal).
22448                    return Some(Err(self.err_at(
22449                        lit_pos,
22450                        alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22451                    )));
22452                };
22453                out.push_str(&alloc::format!("{d:04b}"));
22454            }
22455            out
22456        } else {
22457            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22458                return Some(Err(self.err_at(
22459                    lit_pos,
22460                    alloc::format!("\"{bad}\" is not a valid binary digit"),
22461                )));
22462            }
22463            body
22464        };
22465        // Route through the postfix-cast loop so a chained cast like
22466        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22467        // of erroring at the `::`.
22468        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22469        // literal keeps its exact length, while an explicit `::bit` cast is
22470        // bit(1) with pad/truncate semantics (PG).
22471        Some(self.finish_postfix_casts(Expr::Cast {
22472            expr: Box::new(Expr::Literal(Literal::String(bits))),
22473            target: CastTarget::Named("__bit_literal".to_string()),
22474        }))
22475    }
22476
22477    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22478        if let Some(res) = self.try_parse_bit_string_literal() {
22479            return res;
22480        }
22481        let tok_pos = self.pos;
22482        match self.advance() {
22483            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22484            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22485            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22486            // carrying the source mantissa + scale so no precision is lost. A
22487            // literal too wide for i128 falls back to double precision.
22488            // Out-of-line (#[inline(never)]) — this arm sits on the
22489            // parse_expr recursion chain; its expansion locals must not
22490            // widen the recursive frame (debug frame-cliff discipline).
22491            Token::Numeric(s) => match numeric_token_to_literal(s) {
22492                Ok(lit) => Ok(Expr::Literal(lit)),
22493                Err(msg) => Err(self.err(msg)),
22494            },
22495            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22496            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22497            // (the lexer only emits this token in the MySQL dialect). Lower
22498            // onto the existing bytea cast; out-of-line to keep this arm off
22499            // the parse recursion frame.
22500            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22501            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22502            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22503            Token::Null => Ok(Expr::Literal(Literal::Null)),
22504            // v6.1.1 — `$N` placeholder. The actual Value lookup
22505            // happens in the engine eval path against the prepared-
22506            // statement bind buffer.
22507            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22508            Token::LParen => {
22509                // v4.10: `(SELECT ...)` in expression position is a
22510                // scalar subquery; otherwise it's a parenthesised
22511                // expression. Peek for SELECT keyword to dispatch.
22512                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22513                // lexes as Ident("with") (not a reserved token). The subquery body
22514                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22515                // so its large `Statement` local stays out of parse_atom's stack
22516                // frame — parse_atom is on the recursive `((…))` cycle and the
22517                // nesting budget is tuned to its frame size).
22518                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22519                    if s.eq_ignore_ascii_case("with"));
22520                if matches!(self.peek(), Token::Select) || is_with {
22521                    self.parse_paren_scalar_subquery(is_with)
22522                } else {
22523                    let e = self.parse_expr(0)?;
22524                    // `(a, b, …)` — a row constructor. Valid only
22525                    // in front of a comparison operator or [NOT]
22526                    // IN; both expand at parse time (lexicographic
22527                    // comparison / OR'd row equalities).
22528                    if matches!(self.peek(), Token::Comma) {
22529                        let mut row = alloc::vec![e];
22530                        while matches!(self.peek(), Token::Comma) {
22531                            self.advance();
22532                            row.push(self.parse_expr(0)?);
22533                        }
22534                        if !matches!(self.peek(), Token::RParen) {
22535                            return Err(self.err(alloc::format!(
22536                                "expected ')' after row constructor, got {:?}",
22537                                self.peek()
22538                            )));
22539                        }
22540                        self.advance();
22541                        // A bare `(a, b, …)` row constructor can carry postfix
22542                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22543                        // early return here skips parse_atom's tail postfix
22544                        // pass, so fold casts in explicitly. For the
22545                        // comparison / predicate forms nothing postfix follows,
22546                        // so this is a no-op.
22547                        return self
22548                            .parse_row_comparison_tail(row)
22549                            .and_then(|e| self.finish_postfix_casts(e));
22550                    }
22551                    match self.advance() {
22552                        Token::RParen => Ok(e),
22553                        other => Err(ParseError {
22554                            message: format!("expected ')', got {other:?}"),
22555                            token_pos: self.consumed_pos(),
22556                        }),
22557                    }
22558                }
22559            }
22560            Token::LBracket => self.parse_vector_literal_body(),
22561            Token::Extract => self.parse_extract_atom(),
22562            Token::Interval => self.parse_interval_atom(),
22563            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22564            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22565            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22566            // expression position calling the PG `left(string, n)` /
22567            // `right(string, n)` function; rebuild the AST as a regular
22568            // function call so the engine's apply_function dispatch picks
22569            // it up. Delegated to a #[inline(never)] helper so its locals
22570            // don't bloat this recursive `parse_atom` frame (the nesting
22571            // budget in `enter_nested` is tuned to parse_atom's size).
22572            Token::Left if matches!(self.peek(), Token::LParen) => {
22573                self.parse_lr_string_function_call("left")
22574            }
22575            Token::Right if matches!(self.peek(), Token::LParen) => {
22576                self.parse_lr_string_function_call("right")
22577            }
22578            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22579            // token; we match on the bare ident. NOT is a token
22580            // (consumed in the comparison rung), but `EXISTS (...)`
22581            // at the top of an expression starts here.
22582            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22583                self.parse_exists_atom(false)
22584            }
22585            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22586            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22587            // CASE is a bare ident; we dispatch on lowercase match.
22588            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22589                self.parse_case_atom()
22590            }
22591            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22592            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22593            // '…'`. Lower onto the ::cast node so the existing
22594            // runtime text→date/timestamp paths do the parsing. The
22595            // string must follow immediately, else the ident stays a
22596            // plain column reference.
22597            Token::Ident(s)
22598                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22599                    && matches!(self.peek(), Token::String(_)) =>
22600            {
22601                let target =
22602                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22603                let Token::String(lit) = self.advance() else {
22604                    unreachable!("peek guaranteed a string token");
22605                };
22606                Ok(Expr::Cast {
22607                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22608                    target,
22609                })
22610            }
22611            // v7.39 (round 221) — the SQL-standard long spellings:
22612            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22613            // TIME ZONE '…'`. Consume the modifier and lower to the same
22614            // typed-literal cast (`timetz` / `timestamptz` for WITH).
22615            Token::Ident(s)
22616                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22617                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22618                        || w.eq_ignore_ascii_case("without"))
22619                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22620                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22621                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22622            {
22623                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22624                self.advance(); // WITH / WITHOUT
22625                self.advance(); // TIME
22626                self.advance(); // ZONE
22627                let Token::String(lit) = self.advance() else {
22628                    unreachable!("guard checked a string token");
22629                };
22630                let base = s.to_ascii_lowercase();
22631                let target = match (base.as_str(), with_tz) {
22632                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22633                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22634                    (_, true) => CastTarget::Timestamptz,
22635                    (_, false) => CastTarget::Timestamp,
22636                };
22637                Ok(Expr::Cast {
22638                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22639                    target,
22640                })
22641            }
22642            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
22643            // gathers the subquery's single-column rows (in its row order)
22644            // into an array. Desugared to `array_agg` over the subquery as a
22645            // derived table; out-of-line to keep parse_atom's frame small (it
22646            // sits on the recursive nesting-budget cycle).
22647            Token::Ident(s) | Token::QuotedIdent(s)
22648                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
22649            {
22650                self.parse_array_subquery()
22651            }
22652            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
22653            // is not a reserved token; we match by case-insensitive
22654            // ident. The opening `[` must follow immediately. v7.39 (read01
22655            // round 105) — the body moved out-of-line so its `Vec`/loop locals
22656            // leave parse_atom's frame (which sits on the nesting-budget cycle).
22657            Token::Ident(s) | Token::QuotedIdent(s)
22658                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
22659            {
22660                self.parse_array_literal_body()
22661            }
22662            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
22663            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
22664            // We special-case before the generic ident dispatch so
22665            // the AGAINST clause never reaches the function-call
22666            // loop (which would mis-read `(cols) AGAINST` as a
22667            // call with no trailing modifier). The shape is
22668            // rewritten to a Boolean OR over per-column
22669            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
22670            // term)` so the existing FTS evaluator handles
22671            // semantics — the fulltext-GIN built at CREATE TABLE
22672            // time is currently a "real index that survives dump
22673            // round-trip"; the planner hook that actually uses
22674            // it for posting-list intersection lands in a later
22675            // sub-phase (Phase 2.2b) without touching this surface.
22676            Token::Ident(s) | Token::QuotedIdent(s)
22677                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
22678            {
22679                self.parse_match_against_atom()
22680            }
22681            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
22682            // v7.37.43-T4 — PG-unreserved keywords are legal column /
22683            // alias names in expression context too. `release` appears
22684            // in sentori `0003_partition_events.sql` as both a column
22685            // reference (SELECT … release …) and an INSERT column list
22686            // entry. Mirrors `expect_ident_like`'s expansion of the
22687            // identifier set.
22688            other if unreserved_keyword_text(&other).is_some() => {
22689                let s = unreserved_keyword_text(&other).unwrap();
22690                self.finish_ident_atom(s)
22691            }
22692            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
22693            // only inside `SET` before, so `SELECT @@autocommit` — which
22694            // every MySQL connector asks at handshake — was a parse error.
22695            // MariaDB accepts the bare, `@@session.` and `@@global.`
22696            // spellings alike and answers from the session's own value.
22697            Token::SessionVar(v) => {
22698                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
22699                // has nothing to do with a `@@` engine setting: its own
22700                // per-session namespace, and an unset one reads NULL instead
22701                // of raising. Stripping every `@` (as this did) made `@x` and
22702                // `@@x` the same node, so `SELECT @x` answered "Unknown
22703                // system variable".
22704                Ok(variable_ref_atom(&v))
22705            }
22706            other => Err(ParseError {
22707                message: format!("unexpected token {other:?} in expression"),
22708                token_pos: tok_pos,
22709            }),
22710        }
22711        // After parsing the atom, fold any postfix `::vector` casts.
22712        .and_then(|atom| self.finish_postfix_casts(atom))
22713    }
22714
22715    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
22716    /// Both bind tighter than any binary op.
22717    /// Shared cast-target parser for postfix `::TYPE` and the
22718    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
22719    /// If the next tokens are `( N )`, consume them and return the canonical
22720    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
22721    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
22722    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
22723        if !matches!(self.peek(), Token::LParen) {
22724            return None;
22725        }
22726        self.advance(); // (
22727        let n = match self.advance() {
22728            Token::Integer(n) => n,
22729            _ => return Some(base.to_string()), // malformed → drop precision
22730        };
22731        if matches!(self.peek(), Token::RParen) {
22732            self.advance();
22733        }
22734        Some(alloc::format!("{base}({n})"))
22735    }
22736
22737    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
22738        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
22739        // schema-qualifies every cast target, and `pg_catalog.X` names
22740        // exactly the builtin type X. Consume the qualifier and let
22741        // the ordinary target parse decide.
22742        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
22743            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22744        {
22745            self.advance();
22746            self.advance();
22747        }
22748        let target = match self.advance() {
22749            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
22750                "int" | "integer" | "int4" => {
22751                    if matches!(self.peek(), Token::LBracket)
22752                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22753                    {
22754                        self.advance();
22755                        self.advance();
22756                        CastTarget::IntArray
22757                    } else {
22758                        CastTarget::Int
22759                    }
22760                }
22761                "bigint" | "int8" => {
22762                    if matches!(self.peek(), Token::LBracket)
22763                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22764                    {
22765                        self.advance();
22766                        self.advance();
22767                        CastTarget::BigIntArray
22768                    } else {
22769                        CastTarget::BigInt
22770                    }
22771                }
22772                "float" | "double" => CastTarget::Float,
22773                "text" => {
22774                    // v7.10.11 — `::TEXT[]` widens to TextArray.
22775                    if matches!(self.peek(), Token::LBracket)
22776                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22777                    {
22778                        self.advance();
22779                        self.advance();
22780                        CastTarget::TextArray
22781                    } else {
22782                        CastTarget::Text
22783                    }
22784                }
22785                "bool" | "boolean" => CastTarget::Bool,
22786                "vector" => CastTarget::Vector,
22787                "date" => CastTarget::Date,
22788                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
22789                // seconds precision through the Named path (the engine rounds
22790                // the sub-second field); bare `::timestamp` keeps the fast arm.
22791                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
22792                    Some(named) => CastTarget::Named(named),
22793                    None => CastTarget::Timestamp,
22794                },
22795                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
22796                    Some(named) => CastTarget::Named(named),
22797                    None => CastTarget::Timestamptz,
22798                },
22799                "interval" => CastTarget::Interval,
22800                "json" => CastTarget::Json,
22801                "jsonb" => CastTarget::Jsonb,
22802                // v7.39 (round 694) — these have dedicated CastTarget
22803                // variants, so they never reached the postfix `[]` handling
22804                // further down and `::regtype[]` was a SYNTAX error at the
22805                // `]`. PG has an array type for every scalar; take the
22806                // suffix here and hand the canonical `<ty>_array` name to
22807                // the engine, the same shape every other array cast uses.
22808                "regtype" if self.peek_postfix_array_brackets() => {
22809                    self.advance();
22810                    self.advance();
22811                    CastTarget::Named(alloc::string::String::from("regtype_array"))
22812                }
22813                "regclass" if self.peek_postfix_array_brackets() => {
22814                    self.advance();
22815                    self.advance();
22816                    CastTarget::Named(alloc::string::String::from("regclass_array"))
22817                }
22818                "regtype" => CastTarget::RegType,
22819                "regclass" => CastTarget::RegClass,
22820                // v7.12.0 — `::tsvector` / `::tsquery`.
22821                // Engine decodes the LHS text via the PG
22822                // external form parser.
22823                // v7.39 (round 352, M8) — MySQL's own cast targets.
22824                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
22825                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
22826                // such type, so they are taken only in that dialect and
22827                // fall through to the "type does not exist" arm otherwise.
22828                "signed" | "unsigned" if self.mysql_dialect => {
22829                    if matches!(self.peek(), Token::Ident(k)
22830                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
22831                    {
22832                        self.advance();
22833                    }
22834                    CastTarget::Named(s.to_ascii_lowercase())
22835                }
22836                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
22837                // in MySQL: MariaDB answers '123' where the SQL-standard
22838                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
22839                // Truncating a number to its first digit is a wrong answer
22840                // with no error, so the MySQL session gets MySQL's reading.
22841                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
22842                    CastTarget::Text
22843                }
22844                "tsvector" => CastTarget::TsVector,
22845                "tsquery" => CastTarget::TsQuery,
22846                // v7.17.0 — `::uuid`. Engine decodes the LHS
22847                // text via `spg_storage::parse_uuid_str`.
22848                "uuid" => CastTarget::Uuid,
22849                // v7.18 — `::bytea`. Engine decodes the LHS
22850                // text via the PG hex form (`'\xdeadbeef'`)
22851                // or escape form (`'\\x05\\x00'`). Closes
22852                // mailrs D-pre #3 reverse-acceptance gap.
22853                "bytea" => CastTarget::Bytea,
22854                // v7.37.5 ship triage — generic typed-cast escape.
22855                // Anything the long-tail PG type ident table knows
22856                // about(network/bit/geometry/multirange/etc.)flows
22857                // through `CastTarget::Named(canonical)`; the engine
22858                // resolves via `column_type_to_data_type` and dispatches
22859                // through the typed `coerce_value` path. Truly
22860                // unrecognised idents still hit the error arm below
22861                // because the engine rejects them.
22862                other => {
22863                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
22864                    // `::varchar(255)`, etc. Capture into the canonical
22865                    // `name(p,s)` form so `type_name_to_data_type` can
22866                    // reconstruct the `DataType::Numeric { precision,
22867                    // scale }` (and similar param-carrying types).
22868                    let mut name = other.to_string();
22869                    // v7.39 (round 281) — `::bit varying(3)` is two
22870                    // words; fold the tail in so the typmod reaches the
22871                    // type resolver instead of tripping the parser.
22872                    if name.eq_ignore_ascii_case("bit")
22873                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22874                    {
22875                        self.advance();
22876                        name = alloc::string::String::from("varbit");
22877                    }
22878                    // v7.39 (round 613) — `::character varying` is the same
22879                    // two-word shape and had no fold, so the `varying` was
22880                    // left behind and the cast became a bare `character`,
22881                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
22882                    // `a` where PG answers `ab`. Silently, and for a spelling
22883                    // pg_dump writes.
22884                    if name.eq_ignore_ascii_case("character")
22885                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
22886                    {
22887                        self.advance();
22888                        name = alloc::string::String::from("varchar");
22889                    }
22890                    if matches!(self.peek(), Token::LParen) {
22891                        let mut buf = alloc::string::String::from("(");
22892                        let mut depth = 0usize;
22893                        loop {
22894                            match self.advance() {
22895                                Token::LParen => {
22896                                    depth += 1;
22897                                    if depth > 1 {
22898                                        buf.push('(');
22899                                    }
22900                                }
22901                                Token::RParen => {
22902                                    depth -= 1;
22903                                    if depth == 0 {
22904                                        buf.push(')');
22905                                        break;
22906                                    }
22907                                    buf.push(')');
22908                                }
22909                                Token::Comma => buf.push(','),
22910                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
22911                                // v7.39 (round 273) — a minus used to fall
22912                                // into the catch-all below and vanish, so
22913                                // `::numeric(10,-2)` reached the engine as
22914                                // the text `numeric(10,2)` and silently
22915                                // rounded to two DECIMALS instead of to
22916                                // hundreds. A dropped token is not a
22917                                // no-op when it carries a sign.
22918                                Token::Minus => buf.push('-'),
22919                                Token::Eof => break,
22920                                _ => {}
22921                            }
22922                        }
22923                        name.push_str(&buf);
22924                    }
22925                    // Optional postfix `[]` widens to the array form —
22926                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
22927                    // The engine's `type_name_to_data_type` recognises
22928                    // the canonical `<ty>_array` form.
22929                    if matches!(self.peek(), Token::LBracket)
22930                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22931                    {
22932                        self.advance();
22933                        self.advance();
22934                        name.push_str("_array");
22935                    }
22936                    CastTarget::Named(name)
22937                }
22938            },
22939            Token::Interval => CastTarget::Interval,
22940            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
22941            // "char" (oid 18, SPG Char1 — distinct from bare `char`
22942            // = char(1)); other quoted names resolve like idents.
22943            Token::QuotedIdent(q) => {
22944                if q.eq_ignore_ascii_case("char") {
22945                    CastTarget::Named("char1".into())
22946                } else {
22947                    CastTarget::Named(q.to_ascii_lowercase())
22948                }
22949            }
22950            other => {
22951                return Err(ParseError {
22952                    message: format!("expected type ident after `::`, got {other:?}"),
22953                    token_pos: self.consumed_pos(),
22954                });
22955            }
22956        };
22957        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
22958        // target to its array sibling. Closed-enum arms (Bool /
22959        // SmallInt / Numeric / Float / Date / …) didn't carry the
22960        // explicit widening that Text / Int / BigInt did, so
22961        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
22962        // error. The widening here mirrors the per-arm Text /
22963        // Int / BigInt logic above + folds the new ζ-A first-class
22964        // types through `CastTarget::Named("<ty>_array")`.
22965        if matches!(self.peek(), Token::LBracket)
22966            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
22967        {
22968            let widened = match &target {
22969                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
22970                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
22971                // v7.39 (round 326, V43) — the two temporal types stay
22972                // distinct. Both used to widen to `timestamptz_array`, so
22973                // `::timestamp[]` named the wrong target in its own error
22974                // message and lost the zone-less identity on the way.
22975                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
22976                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
22977                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
22978                CastTarget::Json | CastTarget::Jsonb => {
22979                    Some(CastTarget::Named("jsonb_array".to_string()))
22980                }
22981                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
22982                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
22983                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
22984                CastTarget::Named(name) => {
22985                    let mut a = name.clone();
22986                    a.push_str("_array");
22987                    Some(CastTarget::Named(a))
22988                }
22989                // Int / BigInt / Text / Vector / TsVector / TsQuery /
22990                // RegType / RegClass / TextArray / IntArray /
22991                // BigIntArray already finalised — leave as is.
22992                _ => None,
22993            };
22994            if let Some(w) = widened {
22995                self.advance();
22996                self.advance();
22997                return Ok(w);
22998            }
22999        }
23000        Ok(target)
23001    }
23002
23003    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
23004        loop {
23005            // v7.38 (read01, T9) — composite field access `(expr).field`.
23006            // A bare `a.b` is consumed as a qualified column inside the ident
23007            // atom, so a Dot only survives to this postfix position when the
23008            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
23009            // `.*` whole-row expansion is not handled here (projection-level).
23010            if matches!(self.peek(), Token::Dot)
23011                && matches!(
23012                    self.tokens.get(self.pos + 1),
23013                    Some(Token::Ident(_) | Token::QuotedIdent(_))
23014                )
23015            {
23016                self.advance(); // .
23017                let field = match self.advance() {
23018                    Token::Ident(s) | Token::QuotedIdent(s) => s,
23019                    other => {
23020                        return Err(
23021                            self.err(format!("expected a field name after '.', got {other:?}"))
23022                        );
23023                    }
23024                };
23025                expr = Expr::FieldAccess {
23026                    base: Box::new(expr),
23027                    field,
23028                };
23029                continue;
23030            }
23031            if matches!(self.peek(), Token::DoubleColon) {
23032                self.advance();
23033                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
23034                // target set to include INTERVAL (reserved Token),
23035                // TIMESTAMPTZ, and PG catalog regtype / regclass.
23036                // mailrs follow-up H3a + H3b.
23037                let target = self.parse_cast_target()?;
23038                expr = Expr::Cast {
23039                    expr: Box::new(expr),
23040                    target,
23041                };
23042                continue;
23043            }
23044            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23045            // returns NULL for out-of-range. Multiple subscripts
23046            // chain: `a[i][j]` parses left-to-right.
23047            if matches!(self.peek(), Token::LBracket) {
23048                self.advance();
23049                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23050                // bare index stays a subscript.
23051                let lo = if matches!(self.peek(), Token::Colon) {
23052                    None
23053                } else {
23054                    Some(self.parse_expr(0)?)
23055                };
23056                if matches!(self.peek(), Token::Colon) {
23057                    self.advance();
23058                    let hi = if matches!(self.peek(), Token::RBracket) {
23059                        None
23060                    } else {
23061                        Some(Box::new(self.parse_expr(0)?))
23062                    };
23063                    if !matches!(self.peek(), Token::RBracket) {
23064                        return Err(self.err(alloc::format!(
23065                            "expected ']' after array slice, got {:?}",
23066                            self.peek()
23067                        )));
23068                    }
23069                    self.advance();
23070                    expr = Expr::ArraySlice {
23071                        target: Box::new(expr),
23072                        lo: lo.map(Box::new),
23073                        hi,
23074                    };
23075                    continue;
23076                }
23077                let index = lo.expect("non-colon branch parsed an index");
23078                if !matches!(self.peek(), Token::RBracket) {
23079                    return Err(self.err(alloc::format!(
23080                        "expected ']' after array index, got {:?}",
23081                        self.peek()
23082                    )));
23083                }
23084                self.advance();
23085                expr = Expr::ArraySubscript {
23086                    target: Box::new(expr),
23087                    index: Box::new(index),
23088                };
23089                continue;
23090            }
23091            // `expr AT TIME ZONE zone` — lowers to PG's own function
23092            // form timezone(zone, expr); the scalar implements the
23093            // offset shift (named zones error there — no tzdata).
23094            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23095                && matches!(self.tokens.get(self.pos + 1),
23096                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23097                && matches!(self.tokens.get(self.pos + 2),
23098                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23099            {
23100                self.advance(); // AT
23101                self.advance(); // TIME
23102                self.advance(); // ZONE
23103                // Zone at comparison precedence so AND/OR stay out.
23104                let zone = self.parse_expr(6)?;
23105                expr = Expr::FunctionCall {
23106                    name: "timezone".to_string(),
23107                    args: alloc::vec![zone, expr],
23108                };
23109                continue;
23110            }
23111            // `expr COLLATE "name"` — SPG's single text ordering IS
23112            // byte order, i.e. the C collation. The byte-order
23113            // spellings absorb as no-ops; a locale collation would
23114            // silently sort differently from PG, so it errors
23115            // honestly instead.
23116            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23117                self.advance();
23118                let mut cname = match self.advance() {
23119                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23120                    other => {
23121                        return Err(self.err(alloc::format!(
23122                            "expected collation name after COLLATE, got {other:?}"
23123                        )));
23124                    }
23125                };
23126                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23127                // is how `pg_dump` writes the default one:
23128                // `… COLLATE pg_catalog.default`. Reading a single token
23129                // left the SCHEMA as the name, so the clause was refused
23130                // as an unsupported locale collation and no dump ran.
23131                if matches!(self.peek(), Token::Dot) {
23132                    // v7.39.2 — the qualifier is DROPPED (SPG is single
23133                    // schema) but it is checked first. PostgreSQL 18.6
23134                    // answers `schema "nosuch_schema" does not exist` for
23135                    // one it has never heard of, and dropping it unread
23136                    // meant `COLLATE nosuch_schema."C"` succeeded here —
23137                    // a name that names nothing, accepted.
23138                    let schema = cname.to_ascii_lowercase();
23139                    if !matches!(
23140                        schema.as_str(),
23141                        "pg_catalog" | "public" | "information_schema"
23142                    ) {
23143                        return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23144                    }
23145                    self.advance();
23146                    cname = match self.advance() {
23147                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23148                        // `default` lexes as a KEYWORD, and it is the name
23149                        // pg_dump writes — the same trap round 535 hit with
23150                        // TABLE / INDEX / FULL.
23151                        Token::Default => alloc::string::String::from("default"),
23152                        other => {
23153                            return Err(self.err(alloc::format!(
23154                                "expected collation name after COLLATE, got {other:?}"
23155                            )));
23156                        }
23157                    };
23158                }
23159                let lc = cname.to_ascii_lowercase();
23160                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23161                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23162                // family / `binary`) forces byte-wise, which is exactly
23163                // what `BINARY expr` does — lower onto that so every fold
23164                // site (comparison, LIKE, ORDER BY) suppresses via
23165                // `is_binary_coerced`. A `_ci` family override folds, and
23166                // under the MySQL dialect the default already folds, so it
23167                // absorbs as a no-op; likewise the C / byte-order spellings.
23168                // v7.39.2 — against MySQL's own list, not against the
23169                // shape of the name. `nosuch_bin` took this shortcut and
23170                // became a BINARY cast; `nosuch_ci` took the one below
23171                // and was absorbed as a no-op. Either way the client
23172                // named a collation that does not exist and was told
23173                // nothing. An unknown name now falls through to the
23174                // node, and the engine refuses it.
23175                let real = crate::charset::is_mysql_collation(&lc);
23176                if self.mysql_dialect && real && (lc.ends_with("_bin") || lc == "binary") {
23177                    expr = Expr::Cast {
23178                        expr: alloc::boxed::Box::new(expr),
23179                        target: CastTarget::Named("binary".to_string()),
23180                    };
23181                    continue;
23182                }
23183                let mysql_ci = self.mysql_dialect
23184                    && ((real && lc.ends_with("_ci"))
23185                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23186                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23187                // goes to the lowering channel, the byte-order spellings
23188                // included. Round 691 recorded only the names the old
23189                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23190                // absorbed as a no-op — and once a column could declare a
23191                // collation, absorbing the clause meant the COLUMN's
23192                // collation won where the query had asked for bytes.
23193                if self.in_order_by_key && !mysql_ci {
23194                    self.order_key_collation = Some(cname);
23195                    continue;
23196                }
23197                // v7.39.2 — the clause becomes a NODE rather than being
23198                // refused or absorbed.
23199                //
23200                // What stood here refused the locale names and SILENTLY
23201                // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23202                // answered `t` where PostgreSQL 18.6 answers `f`: the one
23203                // family it let through is the one where dropping it
23204                // changes the answer. Absorbing is only correct when the
23205                // collation asked for is the one the comparison would use
23206                // anyway, and that depends on the DATABASE — which the
23207                // parser cannot see. So it rides along and the engine,
23208                // which can, decides.
23209                //
23210                // `collate_derive` already modelled `Explicit(name)` and
23211                // had no way to be handed one.
23212                // v7.39.2 — a MySQL spelling does not exist on the
23213                // PostgreSQL wire, and THIS is where the wire is known.
23214                //
23215                // The check lived in the evaluator first and asked
23216                // `ctx.mysql_dialect`, which the INSERT path builds as a
23217                // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23218                // in a MySQL session was refused for a collation that
23219                // does not exist on a wire it was not on. Making that
23220                // context truthful would change INSERT-time evaluation
23221                // in other ways as a side effect; the parser already
23222                // gates the introducer on the same flag and is the
23223                // honest place to ask.
23224                if !self.mysql_dialect
23225                    && (lc.ends_with("_ci")
23226                        || lc.ends_with("_cs")
23227                        || lc.ends_with("_bin")
23228                        || lc == "binary"
23229                        || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23230                {
23231                    return Err(self.err(alloc::format!(
23232                        "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23233                    )));
23234                }
23235                // v7.39.3 — the node is built for EVERY name, `_ci`
23236                // included.
23237                //
23238                // A MySQL `_ci` spelling used to be absorbed here on the
23239                // reasoning that a MySQL session folds anyway, so the
23240                // clause asked for what it would have got. That stopped
23241                // being true when the fold learned to read the session's
23242                // collation NAME: under `SET NAMES utf8mb4 COLLATE
23243                // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23244                // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23245                // that would have made it 1 had been dropped in the
23246                // parser. Absorbing is only ever correct when the
23247                // collation asked for is the one the comparison would use
23248                // anyway, and the parser cannot know that — the same
23249                // reasoning already written above for the byte-order
23250                // spellings, applied to the family it had exempted.
23251                expr = Expr::Collate {
23252                    expr: alloc::boxed::Box::new(expr),
23253                    collation: cname,
23254                };
23255                continue;
23256            }
23257            return Ok(expr);
23258        }
23259    }
23260
23261    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23262    /// the first token that is not one. Schema qualifiers collapse to the
23263    /// last part, which is what every other name path here does (SPG is
23264    /// single-schema).
23265    fn take_comma_separated_names(&mut self) -> Vec<String> {
23266        let mut out = Vec::new();
23267        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23268            self.advance();
23269            let mut last = n;
23270            while matches!(self.peek(), Token::Dot) {
23271                self.advance();
23272                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23273                    last = t;
23274                }
23275            }
23276            out.push(last);
23277            if matches!(self.peek(), Token::Comma) {
23278                self.advance();
23279            } else {
23280                break;
23281            }
23282        }
23283        out
23284    }
23285
23286    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23287    ///
23288    /// The general cast-target path tests this inline; the types with their
23289    /// own `CastTarget` variant need it as a guard on their match arm,
23290    /// which is what this exists for.
23291    fn peek_postfix_array_brackets(&self) -> bool {
23292        matches!(self.peek(), Token::LBracket)
23293            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23294    }
23295
23296    /// Parse the operator tail after a `(a, b, …)` row constructor
23297    /// and expand at parse time. `=` is the conjunction of element
23298    /// equalities; `<>` its negation; the order operators expand
23299    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23300    /// equalities. Anything else (a bare row value, a subquery
23301    /// RHS) errors honestly — SPG has no composite runtime value.
23302    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23303        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23304            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23305                lhs: Box::new(l.clone()),
23306                op: BinOp::Eq,
23307                rhs: Box::new(r.clone()),
23308            });
23309            let first = it.next().expect("row has at least two elements");
23310            it.fold(first, |acc, e| Expr::Binary {
23311                lhs: Box::new(acc),
23312                op: BinOp::And,
23313                rhs: Box::new(e),
23314            })
23315        }
23316        // Lexicographic (a,b) OP (c,d):
23317        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
23318        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23319            if lhs.len() == 1 {
23320                return Expr::Binary {
23321                    lhs: Box::new(lhs[0].clone()),
23322                    op: last,
23323                    rhs: Box::new(rhs[0].clone()),
23324                };
23325            }
23326            let head_strict = Expr::Binary {
23327                lhs: Box::new(lhs[0].clone()),
23328                op: strict,
23329                rhs: Box::new(rhs[0].clone()),
23330            };
23331            let head_eq = Expr::Binary {
23332                lhs: Box::new(lhs[0].clone()),
23333                op: BinOp::Eq,
23334                rhs: Box::new(rhs[0].clone()),
23335            };
23336            Expr::Binary {
23337                lhs: Box::new(head_strict),
23338                op: BinOp::Or,
23339                rhs: Box::new(Expr::Binary {
23340                    lhs: Box::new(head_eq),
23341                    op: BinOp::And,
23342                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23343                }),
23344            }
23345        }
23346        let negated_in = if matches!(self.peek(), Token::Not)
23347            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23348        {
23349            self.advance();
23350            true
23351        } else {
23352            false
23353        };
23354        if matches!(self.peek(), Token::In) {
23355            self.advance();
23356            if !matches!(self.peek(), Token::LParen) {
23357                return Err(self.err(alloc::format!(
23358                    "expected '(' after row IN, got {:?}",
23359                    self.peek()
23360                )));
23361            }
23362            self.advance();
23363            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23364            // not a list of literal rows. Row-vs-list decomposes to
23365            // OR-of-AND above, but the subquery's rows are only known at
23366            // runtime, so keep it as a RowInSubquery node.
23367            if matches!(self.peek(), Token::Select) {
23368                let inner = self.parse_select_stmt()?;
23369                if !matches!(self.peek(), Token::RParen) {
23370                    return Err(self.err(alloc::format!(
23371                        "expected ')' after row IN-subquery, got {:?}",
23372                        self.peek()
23373                    )));
23374                }
23375                self.advance();
23376                let Statement::Select(s) = inner else {
23377                    unreachable!("parse_select_stmt always returns Statement::Select")
23378                };
23379                return Ok(Expr::RowInSubquery {
23380                    row,
23381                    subquery: Box::new(s),
23382                    negated: negated_in,
23383                });
23384            }
23385            let mut alternatives: Vec<Expr> = Vec::new();
23386            loop {
23387                // Optional ROW keyword before the paren row.
23388                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23389                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23390                {
23391                    self.advance();
23392                }
23393                if !matches!(self.peek(), Token::LParen) {
23394                    return Err(self.err(alloc::format!(
23395                        "expected '(' to open a row inside IN, got {:?}",
23396                        self.peek()
23397                    )));
23398                }
23399                self.advance();
23400                let mut rhs = alloc::vec![self.parse_expr(0)?];
23401                while matches!(self.peek(), Token::Comma) {
23402                    self.advance();
23403                    rhs.push(self.parse_expr(0)?);
23404                }
23405                if !matches!(self.peek(), Token::RParen) {
23406                    return Err(self.err(alloc::format!(
23407                        "expected ')' after row inside IN, got {:?}",
23408                        self.peek()
23409                    )));
23410                }
23411                self.advance();
23412                if rhs.len() != row.len() {
23413                    return Err(self.err(alloc::format!(
23414                        "row IN arity mismatch: left has {}, right has {}",
23415                        row.len(),
23416                        rhs.len()
23417                    )));
23418                }
23419                alternatives.push(row_eq(&row, &rhs));
23420                if matches!(self.peek(), Token::Comma) {
23421                    self.advance();
23422                    continue;
23423                }
23424                break;
23425            }
23426            if !matches!(self.peek(), Token::RParen) {
23427                return Err(self.err(alloc::format!(
23428                    "expected ')' to close row IN list, got {:?}",
23429                    self.peek()
23430                )));
23431            }
23432            self.advance();
23433            let mut it = alternatives.into_iter();
23434            let first = it.next().expect("IN list has at least one row");
23435            let combined = it.fold(first, |acc, e| Expr::Binary {
23436                lhs: Box::new(acc),
23437                op: BinOp::Or,
23438                rhs: Box::new(e),
23439            });
23440            return Ok(maybe_not(combined, negated_in));
23441        }
23442        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23443        // two periods share at least one time point. Each pair is
23444        // normalised with least/greatest (PG accepts the endpoints
23445        // in either order), then lowered to the standard
23446        // `start1 < end2 AND start2 < end1` form.
23447        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23448            if row.len() != 2 {
23449                return Err(self.err(alloc::format!(
23450                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
23451                    row.len()
23452                )));
23453            }
23454            self.advance();
23455            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23456                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23457            {
23458                self.advance();
23459            }
23460            if !matches!(self.peek(), Token::LParen) {
23461                return Err(self.err(alloc::format!(
23462                    "expected '(' after OVERLAPS, got {:?}",
23463                    self.peek()
23464                )));
23465            }
23466            self.advance();
23467            let r0 = self.parse_expr(0)?;
23468            if !matches!(self.peek(), Token::Comma) {
23469                return Err(self.err(alloc::format!(
23470                    "OVERLAPS needs (start, end) on the right, got {:?}",
23471                    self.peek()
23472                )));
23473            }
23474            self.advance();
23475            let r1 = self.parse_expr(0)?;
23476            if !matches!(self.peek(), Token::RParen) {
23477                return Err(self.err(alloc::format!(
23478                    "expected ')' after OVERLAPS pair, got {:?}",
23479                    self.peek()
23480                )));
23481            }
23482            self.advance();
23483            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23484                name: String::from(name),
23485                args: alloc::vec![a.clone(), b.clone()],
23486            };
23487            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23488                lhs: Box::new(lhs),
23489                op: BinOp::Lt,
23490                rhs: Box::new(rhs),
23491            };
23492            return Ok(Expr::Binary {
23493                lhs: Box::new(lt(
23494                    pair_fn("least", &row[0], &row[1]),
23495                    pair_fn("greatest", &r0, &r1),
23496                )),
23497                op: BinOp::And,
23498                rhs: Box::new(lt(
23499                    pair_fn("least", &r0, &r1),
23500                    pair_fn("greatest", &row[0], &row[1]),
23501                )),
23502            });
23503        }
23504        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23505        // PG, `IS NULL` is true only when EVERY field is NULL, and
23506        // `IS NOT NULL` is true only when every field is non-NULL — the
23507        // latter is NOT the negation of the former (a mixed row is
23508        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23509        // which reproduces exactly that all-fields semantics.
23510        if matches!(self.peek(), Token::Is) {
23511            self.advance();
23512            let negated = if matches!(self.peek(), Token::Not) {
23513                self.advance();
23514                true
23515            } else {
23516                false
23517            };
23518            if !matches!(self.peek(), Token::Null) {
23519                return Err(self.err(alloc::format!(
23520                    "expected NULL after row IS [NOT], got {:?}",
23521                    self.peek()
23522                )));
23523            }
23524            self.advance();
23525            let mut it = row.iter().map(|e| Expr::IsNull {
23526                expr: Box::new(e.clone()),
23527                negated,
23528            });
23529            let first = it.next().expect("row has at least two elements");
23530            return Ok(it.fold(first, |acc, e| Expr::Binary {
23531                lhs: Box::new(acc),
23532                op: BinOp::And,
23533                rhs: Box::new(e),
23534            }));
23535        }
23536        let op = match self.peek() {
23537            Token::Eq => BinOp::Eq,
23538            Token::NotEq => BinOp::NotEq,
23539            Token::Lt => BinOp::Lt,
23540            Token::LtEq => BinOp::LtEq,
23541            Token::Gt => BinOp::Gt,
23542            Token::GtEq => BinOp::GtEq,
23543            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23544            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23545            // constructor value, identical to the `ROW(a, b, …)` keyword form:
23546            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23547            // (`::text`, `.field`) applies at the caller just as it does for the
23548            // ROW(...) node. All the comparison / predicate forms returned above.
23549            _ => {
23550                return Ok(Expr::FunctionCall {
23551                    name: String::from("row"),
23552                    args: row,
23553                });
23554            }
23555        };
23556        self.advance();
23557        // Optional ROW keyword before the paren row.
23558        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23559            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23560        {
23561            self.advance();
23562        }
23563        if !matches!(self.peek(), Token::LParen) {
23564            return Err(self.err(alloc::format!(
23565                "expected '(' to open the right-hand row, got {:?}",
23566                self.peek()
23567            )));
23568        }
23569        self.advance();
23570        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23571        // subquery. Kept as a node (the subquery's row is a runtime value);
23572        // the literal-RHS form below still decomposes at parse time.
23573        if matches!(self.peek(), Token::Select) {
23574            let inner = self.parse_select_stmt()?;
23575            if !matches!(self.peek(), Token::RParen) {
23576                return Err(self.err(alloc::format!(
23577                    "expected ')' after row comparison subquery, got {:?}",
23578                    self.peek()
23579                )));
23580            }
23581            self.advance();
23582            let Statement::Select(s) = inner else {
23583                unreachable!("parse_select_stmt always returns Statement::Select")
23584            };
23585            return Ok(Expr::RowCmpSubquery {
23586                row,
23587                op,
23588                subquery: Box::new(s),
23589            });
23590        }
23591        let mut rhs = alloc::vec![self.parse_expr(0)?];
23592        while matches!(self.peek(), Token::Comma) {
23593            self.advance();
23594            rhs.push(self.parse_expr(0)?);
23595        }
23596        if !matches!(self.peek(), Token::RParen) {
23597            return Err(self.err(alloc::format!(
23598                "expected ')' after right-hand row, got {:?}",
23599                self.peek()
23600            )));
23601        }
23602        self.advance();
23603        if rhs.len() != row.len() {
23604            // v7.39 (round 239) — PG's wording (42601).
23605            return Err(self.err("unequal number of entries in row expressions".to_string()));
23606        }
23607        Ok(match op {
23608            BinOp::Eq => row_eq(&row, &rhs),
23609            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23610            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23611            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23612            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23613            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23614            _ => unreachable!("op restricted above"),
23615        })
23616    }
23617
23618    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23619    /// escape character becomes the matcher's default backslash:
23620    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23621    /// → the char itself, and any pre-existing backslash escapes
23622    /// itself so it stays literal. Both operands must be string
23623    /// literals — a runtime pattern would need matcher support.
23624    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
23625        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
23626            (&pattern, &esc)
23627        else {
23628            return Err(
23629                "LIKE ... ESCAPE requires string-literal pattern and escape \
23630                 (runtime escape characters are not supported yet)"
23631                    .into(),
23632            );
23633        };
23634        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
23635        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
23636        // multi-character escape is an error.
23637        let esc_ch: Option<char> = {
23638            let mut ch_iter = e.chars();
23639            match (ch_iter.next(), ch_iter.next()) {
23640                (Some(c), None) => Some(c),
23641                (None, _) => None,
23642                (Some(_), Some(_)) => {
23643                    return Err(alloc::format!(
23644                        "ESCAPE must be a single character, got {e:?}"
23645                    ));
23646                }
23647            }
23648        };
23649        let mut out = String::with_capacity(p.len() + 4);
23650        let mut chars = p.chars();
23651        while let Some(c) = chars.next() {
23652            if Some(c) == esc_ch {
23653                match chars.next() {
23654                    // Escaped wildcard / escaped escape → keep the
23655                    // next char literal via backslash.
23656                    Some(next) => {
23657                        out.push('\\');
23658                        out.push(next);
23659                    }
23660                    None => {
23661                        return Err("LIKE pattern ends with the escape character".into());
23662                    }
23663                }
23664            } else if c == '\\' && esc_ch != Some('\\') {
23665                // A raw backslash is literal under a custom (or absent) escape
23666                // — escape it for the backslash-based matcher.
23667                out.push('\\');
23668                out.push('\\');
23669            } else {
23670                out.push(c);
23671            }
23672        }
23673        Ok(Expr::Literal(Literal::String(out)))
23674    }
23675
23676    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
23677    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
23678    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
23679    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
23680    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
23681    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
23682    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
23683    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
23684    /// array expression errors honestly rather than silently mismatching.
23685    fn try_like_any_all(
23686        &mut self,
23687        base: &Expr,
23688        negated: bool,
23689        case_insensitive: bool,
23690    ) -> Result<Option<Expr>, ParseError> {
23691        let is_any = match self.peek() {
23692            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
23693            Token::Ident(s)
23694                if s.eq_ignore_ascii_case("any")
23695                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
23696            {
23697                true
23698            }
23699            _ => return Ok(None),
23700        };
23701        self.advance(); // ANY / ALL
23702        self.advance(); // '('
23703        let arr = self.parse_expr(0)?;
23704        if !matches!(self.peek(), Token::RParen) {
23705            return Err(self.err(format!(
23706                "expected ')' after LIKE {} argument, got {:?}",
23707                if is_any { "ANY" } else { "ALL" },
23708                self.peek()
23709            )));
23710        }
23711        self.advance(); // ')'
23712        let Expr::Array(items) = arr else {
23713            return Err(self.err(
23714                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
23715            ));
23716        };
23717        let mut clauses = items.into_iter().map(|p| Expr::Like {
23718            expr: Box::new(base.clone()),
23719            pattern: Box::new(p),
23720            negated,
23721            case_insensitive,
23722        });
23723        let Some(first) = clauses.next() else {
23724            // ANY(empty) = FALSE, ALL(empty) = TRUE.
23725            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
23726        };
23727        let op = if is_any { BinOp::Or } else { BinOp::And };
23728        let combined = clauses.fold(first, |acc, c| Expr::Binary {
23729            lhs: Box::new(acc),
23730            op,
23731            rhs: Box::new(c),
23732        });
23733        Ok(Some(combined))
23734    }
23735
23736    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
23737    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
23738    /// `AND` is not swallowed.
23739    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
23740        self.advance(); // BETWEEN
23741        // SYMMETRIC — the bounds may arrive in either order; both
23742        // orientations OR together. ASYMMETRIC is the default and
23743        // absorbs as noise.
23744        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
23745        {
23746            self.advance();
23747            true
23748        } else {
23749            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
23750                self.advance();
23751            }
23752            false
23753        };
23754        let low = self.parse_expr(6)?;
23755        if !matches!(self.peek(), Token::And) {
23756            return Err(self.err(format!(
23757                "expected AND after BETWEEN low bound, got {:?}",
23758                self.peek()
23759            )));
23760        }
23761        self.advance();
23762        let high = self.parse_expr(6)?;
23763        let target = Box::new(expr);
23764        let range = |lo: Expr, hi: Expr| Expr::Binary {
23765            lhs: Box::new(Expr::Binary {
23766                lhs: target.clone(),
23767                op: BinOp::GtEq,
23768                rhs: Box::new(lo),
23769            }),
23770            op: BinOp::And,
23771            rhs: Box::new(Expr::Binary {
23772                lhs: target.clone(),
23773                op: BinOp::LtEq,
23774                rhs: Box::new(hi),
23775            }),
23776        };
23777        let combined = if symmetric {
23778            Expr::Binary {
23779                lhs: Box::new(range(low.clone(), high.clone())),
23780                op: BinOp::Or,
23781                rhs: Box::new(range(high, low)),
23782            }
23783        } else {
23784            range(low, high)
23785        };
23786        Ok(maybe_not(combined, negated))
23787    }
23788
23789    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
23790    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
23791    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
23792    /// Caller already consumed the leading `WITH` ident.
23793    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
23794    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
23795    /// self-reference that appears more than once in a single term.
23796    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
23797        use crate::ast::{CteBody, SelectStatement};
23798        if !cte.recursive {
23799            return Ok(());
23800        }
23801        let CteBody::Select(body) = &cte.body else {
23802            return Ok(());
23803        };
23804        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
23805        // check the anchor and every peer term.
23806        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
23807        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
23808        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
23809            return Err(self.err(String::from(
23810                "ORDER BY in a recursive query is not implemented",
23811            )));
23812        }
23813        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
23814            return Err(self.err(String::from(
23815                "LIMIT in a recursive query is not implemented",
23816            )));
23817        }
23818        let self_refs = |s: &SelectStatement| -> usize {
23819            let Some(from) = &s.from else {
23820                return 0;
23821            };
23822            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
23823            for j in &from.joins {
23824                if j.table.name.eq_ignore_ascii_case(&cte.name) {
23825                    n += 1;
23826                }
23827            }
23828            n
23829        };
23830        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
23831            return Err(self.err(alloc::format!(
23832                "recursive reference to query \"{}\" must not appear more than once",
23833                cte.name
23834            )));
23835        }
23836        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
23837        // apply only when the body actually references itself (a non-self-
23838        // referencing CTE under WITH RECURSIVE may use any set-op shape).
23839        let anchor_refs = self_refs(body);
23840        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
23841        if anchor_refs > 0 || union_refs {
23842            // Shape: the top level must be UNION [ALL] arms only. A self-ref
23843            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
23844            // "does not have the form" error — SPG used to compute a value.
23845            if body.unions.is_empty()
23846                || body.unions.iter().any(|(k, _)| {
23847                    !matches!(
23848                        k,
23849                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
23850                    )
23851                })
23852            {
23853                return Err(self.err(alloc::format!(
23854                    "recursive query \"{}\" does not have the form non-recursive-term \
23855                     UNION [ALL] recursive-term",
23856                    cte.name
23857                )));
23858            }
23859            if anchor_refs > 0 {
23860                return Err(self.err(alloc::format!(
23861                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
23862                    cte.name
23863                )));
23864            }
23865        }
23866        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
23867        for (_, u) in &body.unions {
23868            if self_refs(u) == 0 {
23869                continue;
23870            }
23871            // The self-reference must not sit on the nullable side of an outer
23872            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
23873            if let Some(from) = &u.from {
23874                for (i, j) in from.joins.iter().enumerate() {
23875                    let left_has_self = is_self(&from.primary)
23876                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
23877                    let violated = match j.kind {
23878                        crate::ast::JoinKind::Left => is_self(&j.table),
23879                        crate::ast::JoinKind::Right => left_has_self,
23880                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
23881                        _ => false,
23882                    };
23883                    if violated {
23884                        return Err(self.err(alloc::format!(
23885                            "recursive reference to query \"{}\" must not appear within an outer join",
23886                            cte.name
23887                        )));
23888                    }
23889                }
23890            }
23891            // No aggregates at the top level of the recursive term (SPG used
23892            // to run them and surface a misleading downstream error).
23893            let mut items_and_having: Vec<&Expr> = Vec::new();
23894            for it in &u.items {
23895                if let crate::ast::SelectItem::Expr { expr, .. } = it {
23896                    items_and_having.push(expr);
23897                }
23898            }
23899            if let Some(h) = &u.having {
23900                items_and_having.push(h);
23901            }
23902            for e in items_and_having {
23903                if expr_has_toplevel_aggregate(e) {
23904                    return Err(self.err(String::from(
23905                        "aggregate functions are not allowed in a recursive query's recursive term",
23906                    )));
23907                }
23908            }
23909        }
23910        // A self-reference inside a sublink expression (EXISTS / IN / scalar
23911        // subquery) anywhere in the body is rejected; a plain FROM derived
23912        // table is legal in PG and untouched here.
23913        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
23914        all_terms.extend(body.unions.iter().map(|(_, u)| u));
23915        for term in all_terms {
23916            if select_has_self_ref_in_sublink(term, &cte.name) {
23917                return Err(self.err(alloc::format!(
23918                    "recursive reference to query \"{}\" must not appear within a subquery",
23919                    cte.name
23920                )));
23921            }
23922        }
23923        Ok(())
23924    }
23925
23926    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
23927    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
23928    /// right after parse so the engine sees a plain recursive CTE with the
23929    /// tracking columns already projected. DEPTH FIRST and CYCLE are
23930    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
23931    /// text-rendered rows can't provide, and errors honestly.
23932    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
23933        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
23934        if cte.search.is_none() && cte.cycle.is_none() {
23935            return Ok(());
23936        }
23937        let cte_name = cte.name.clone();
23938        let col_names = cte.column_overrides.clone();
23939        if col_names.is_empty() {
23940            return Err(
23941                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
23942            );
23943        }
23944        let search = cte.search.take();
23945        let cycle = cte.cycle.take();
23946        let mut extra_cols: Vec<String> = Vec::new();
23947        let col_ref = |name: &str| {
23948            Expr::Column(ColumnName {
23949                qualifier: Some(cte_name.clone()),
23950                name: name.to_string(),
23951            })
23952        };
23953        // Position of a SEARCH/CYCLE column within the CTE's column list.
23954        let pos_of = |name: &str| -> Result<usize, ParseError> {
23955            col_names
23956                .iter()
23957                .position(|c| c.eq_ignore_ascii_case(name))
23958                .ok_or_else(|| {
23959                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
23960                })
23961        };
23962        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
23963            let mut args = Vec::with_capacity(positions.len());
23964            for &p in positions {
23965                match items.get(p) {
23966                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
23967                    _ => {
23968                        return Err(self.err(
23969                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
23970                        ));
23971                    }
23972                }
23973            }
23974            Ok(Expr::FunctionCall {
23975                name: "row".into(),
23976                args,
23977            })
23978        };
23979        let CteBody::Select(body) = &mut cte.body else {
23980            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
23981        };
23982        if body.unions.is_empty() {
23983            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
23984        }
23985        let rec = body.unions.len() - 1; // recursive term = last UNION peer
23986
23987        if let Some(srch) = search {
23988            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
23989            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
23990            // no typed `record[]`, but element-wise array ORDER BY is correct
23991            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
23992            // exactly onto a typed array: DEPTH is the root→node path
23993            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
23994            // orders numerically (multi-digit keys included), matching PG.
23995            //
23996            // A multi-column BY would need a record[] to keep the per-node key
23997            // tuple orderable, which SPG can't express — error honestly there
23998            // rather than mis-order.
23999            if srch.by_columns.len() != 1 {
24000                return Err(self.err(
24001                    "SEARCH … BY with multiple columns needs typed record[] ordering \
24002                     SPG doesn't have yet; a single BY column is supported"
24003                        .into(),
24004                ));
24005            }
24006            let key_pos = pos_of(&srch.by_columns[0])?;
24007            let base_key = match body.items.get(key_pos) {
24008                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24009                _ => {
24010                    return Err(
24011                        self.err("SEARCH BY column maps to a non-expression select item".into())
24012                    );
24013                }
24014            };
24015            let rec_key = match body.unions[rec].1.items.get(key_pos) {
24016                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24017                _ => {
24018                    return Err(
24019                        self.err("SEARCH BY column maps to a non-expression select item".into())
24020                    );
24021                }
24022            };
24023            if srch.depth_first {
24024                // base: ARRAY[key]; rec: array_append(cte.set, key).
24025                body.items.push(SelectItem::Expr {
24026                    expr: Expr::Array(alloc::vec![base_key]),
24027                    alias: Some(srch.set_column.clone()),
24028                });
24029                body.unions[rec].1.items.push(SelectItem::Expr {
24030                    expr: Expr::FunctionCall {
24031                        name: "array_append".into(),
24032                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
24033                    },
24034                    alias: Some(srch.set_column.clone()),
24035                });
24036            } else {
24037                // BREADTH: [depth, key]; depth starts at 0 and increments. The
24038                // leading depth element dominates the element-wise comparison,
24039                // so shallower rows sort first, then by key — PG's (depth, key).
24040                body.items.push(SelectItem::Expr {
24041                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24042                    alias: Some(srch.set_column.clone()),
24043                });
24044                // rec depth = cte.set[1] + 1.
24045                let parent_depth = Expr::ArraySubscript {
24046                    target: Box::new(col_ref(&srch.set_column)),
24047                    index: Box::new(Expr::Literal(Literal::Integer(1))),
24048                };
24049                body.unions[rec].1.items.push(SelectItem::Expr {
24050                    expr: Expr::Array(alloc::vec![
24051                        Expr::Binary {
24052                            lhs: Box::new(parent_depth),
24053                            op: BinOp::Add,
24054                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24055                        },
24056                        rec_key,
24057                    ]),
24058                    alias: Some(srch.set_column.clone()),
24059                });
24060            }
24061            extra_cols.push(srch.set_column);
24062        }
24063
24064        if let Some(cyc) = cycle {
24065            let positions: Vec<usize> = cyc
24066                .columns
24067                .iter()
24068                .map(|c| pos_of(c))
24069                .collect::<Result<_, _>>()?;
24070            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24071            // cast it to text for the cycle path: membership only needs equality,
24072            // and the record text form gives SPG a TextArray path (SPG has no
24073            // typed record[] array). Cycle detection is unaffected.
24074            let base_row = Expr::Cast {
24075                expr: Box::new(row_of(&body.items, &positions)?),
24076                target: CastTarget::Text,
24077            };
24078            let rec_row = Expr::Cast {
24079                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24080                target: CastTarget::Text,
24081            };
24082            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24083            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24084            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24085            body.items.push(SelectItem::Expr {
24086                expr: Expr::Literal(dflt.clone()),
24087                alias: Some(cyc.mark_column.clone()),
24088            });
24089            body.items.push(SelectItem::Expr {
24090                expr: Expr::Array(alloc::vec![base_row]),
24091                alias: Some(cyc.path_column.clone()),
24092            });
24093            // rec mark: ROW(cols) already in the path → cycle.
24094            let hit = Expr::AnyAll {
24095                expr: Box::new(rec_row.clone()),
24096                op: BinOp::Eq,
24097                array: Box::new(col_ref(&cyc.path_column)),
24098                is_any: true,
24099            };
24100            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24101                Expr::Case {
24102                    operand: None,
24103                    branches: alloc::vec![(hit, Expr::Literal(mark))],
24104                    else_branch: Some(Box::new(Expr::Literal(dflt))),
24105                }
24106            } else {
24107                hit
24108            };
24109            body.unions[rec].1.items.push(SelectItem::Expr {
24110                expr: mark_expr,
24111                alias: Some(cyc.mark_column.clone()),
24112            });
24113            // rec path: array_append(cte.path, ROW(cols)).
24114            body.unions[rec].1.items.push(SelectItem::Expr {
24115                expr: Expr::FunctionCall {
24116                    name: "array_append".into(),
24117                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24118                },
24119                alias: Some(cyc.path_column.clone()),
24120            });
24121            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24122            let stop = Expr::Unary {
24123                op: UnOp::Not,
24124                expr: Box::new(col_ref(&cyc.mark_column)),
24125            };
24126            let w = &mut body.unions[rec].1.where_;
24127            *w = Some(match w.take() {
24128                Some(prev) => Expr::Binary {
24129                    lhs: Box::new(prev),
24130                    op: BinOp::And,
24131                    rhs: Box::new(stop),
24132                },
24133                None => stop,
24134            });
24135            extra_cols.push(cyc.mark_column);
24136            extra_cols.push(cyc.path_column);
24137        }
24138        cte.column_overrides.extend(extra_cols);
24139        Ok(())
24140    }
24141
24142    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24143    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24144    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24145        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24146            return Ok(None);
24147        }
24148        self.advance(); // SEARCH
24149        let depth_first = match self.peek() {
24150            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24151            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24152            other => {
24153                return Err(self.err(format!(
24154                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24155                )));
24156            }
24157        };
24158        self.advance();
24159        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24160            return Err(self.err(format!(
24161                "expected FIRST after SEARCH mode, got {:?}",
24162                self.peek()
24163            )));
24164        }
24165        self.advance();
24166        if !self.peek_is_by() {
24167            return Err(self.err(format!(
24168                "expected BY after SEARCH … FIRST, got {:?}",
24169                self.peek()
24170            )));
24171        }
24172        self.advance();
24173        let mut by_columns = alloc::vec![self.expect_ident_like()?];
24174        while matches!(self.peek(), Token::Comma) {
24175            self.advance();
24176            by_columns.push(self.expect_ident_like()?);
24177        }
24178        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24179            return Err(self.err(format!(
24180                "expected SET in SEARCH clause, got {:?}",
24181                self.peek()
24182            )));
24183        }
24184        self.advance();
24185        let set_column = self.expect_ident_like()?;
24186        Ok(Some(crate::ast::SearchClause {
24187            depth_first,
24188            by_columns,
24189            set_column,
24190        }))
24191    }
24192
24193    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24194    /// USING pathcol`. Returns None when the next token isn't CYCLE.
24195    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24196        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24197            return Ok(None);
24198        }
24199        self.advance(); // CYCLE
24200        let mut columns = alloc::vec![self.expect_ident_like()?];
24201        while matches!(self.peek(), Token::Comma) {
24202            self.advance();
24203            columns.push(self.expect_ident_like()?);
24204        }
24205        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24206            return Err(self.err(format!(
24207                "expected SET in CYCLE clause, got {:?}",
24208                self.peek()
24209            )));
24210        }
24211        self.advance();
24212        let mark_column = self.expect_ident_like()?;
24213        let mut mark_value = None;
24214        let mut default_value = None;
24215        if matches!(self.peek(), Token::To) {
24216            self.advance();
24217            mark_value = Some(self.parse_cycle_literal()?);
24218            if !matches!(self.peek(), Token::Default) {
24219                return Err(self.err(format!(
24220                    "expected DEFAULT after CYCLE … TO, got {:?}",
24221                    self.peek()
24222                )));
24223            }
24224            self.advance();
24225            default_value = Some(self.parse_cycle_literal()?);
24226        }
24227        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24228            return Err(self.err(format!(
24229                "expected USING in CYCLE clause, got {:?}",
24230                self.peek()
24231            )));
24232        }
24233        self.advance();
24234        let path_column = self.expect_ident_like()?;
24235        Ok(Some(crate::ast::CycleClause {
24236            columns,
24237            mark_column,
24238            mark_value,
24239            default_value,
24240            path_column,
24241        }))
24242    }
24243
24244    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24245    /// literal (string / bool / number) in PG.
24246    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24247        match self.parse_expr(0)? {
24248            Expr::Literal(l) => Ok(l),
24249            other => Err(self.err(format!(
24250                "CYCLE mark/default value must be a literal, got {other:?}"
24251            ))),
24252        }
24253    }
24254
24255    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24256        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24257        // Comes through as an identifier; consume it if present and
24258        // mark every CTE in the clause as recursive (PG semantics —
24259        // the flag is per-WITH, not per-CTE).
24260        let mut recursive = false;
24261        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24262            && s.eq_ignore_ascii_case("recursive")
24263        {
24264            self.advance();
24265            recursive = true;
24266        }
24267        let mut ctes = Vec::new();
24268        loop {
24269            let name = self.expect_ident_like()?;
24270            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24271            // PG uses these to rename the body's output columns; we
24272            // do the same below by overriding `columns[i].name`.
24273            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24274                self.advance();
24275                let mut names = Vec::new();
24276                loop {
24277                    names.push(self.expect_ident_like()?);
24278                    if matches!(self.peek(), Token::Comma) {
24279                        self.advance();
24280                        continue;
24281                    }
24282                    break;
24283                }
24284                if !matches!(self.peek(), Token::RParen) {
24285                    return Err(self.err(format!(
24286                        "expected ')' to close CTE column list, got {:?}",
24287                        self.peek()
24288                    )));
24289                }
24290                self.advance();
24291                names
24292            } else {
24293                Vec::new()
24294            };
24295            // AS is a reserved Token::As (used by SELECT-item / FROM
24296            // aliasing) — handle it specially rather than as a bare
24297            // ident.
24298            if !matches!(self.peek(), Token::As) {
24299                return Err(self.err(format!(
24300                    "expected AS after CTE name {name:?}, got {:?}",
24301                    self.peek()
24302                )));
24303            }
24304            self.advance();
24305            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24306            // MATERIALIZED` optimizer hints. SPG materialises every
24307            // CTE, so both spellings are accepted and absorbed.
24308            if matches!(self.peek(), Token::Not) {
24309                self.advance(); // NOT
24310                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24311                    if s.eq_ignore_ascii_case("materialized"))
24312                {
24313                    self.advance();
24314                } else {
24315                    return Err(self.err(format!(
24316                        "expected MATERIALIZED after AS NOT, got {:?}",
24317                        self.peek()
24318                    )));
24319                }
24320            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24321                if s.eq_ignore_ascii_case("materialized"))
24322            {
24323                self.advance();
24324            }
24325            if !matches!(self.peek(), Token::LParen) {
24326                return Err(self.err(format!(
24327                    "expected '(' after AS in WITH clause, got {:?}",
24328                    self.peek()
24329                )));
24330            }
24331            self.advance();
24332            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24333            // RETURNING) as the CTE body in addition to SELECT.
24334            // PG writable CTE semantics. UPDATE / DELETE come in as
24335            // bare Idents (lexer keeps SELECT / INSERT as reserved
24336            // tokens but treats the rest of DML as case-insensitive
24337            // idents).
24338            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24339            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24340            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24341            let body = match self.peek() {
24342                Token::Select => {
24343                    let inner = self.parse_select_stmt()?;
24344                    let Statement::Select(s) = inner else {
24345                        unreachable!("parse_select_stmt returns Select");
24346                    };
24347                    crate::ast::CteBody::Select(s)
24348                }
24349                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24350                // `SELECT * FROM t` this way and accepts it wherever a
24351                // SELECT goes, so the CTE body dispatch needs its own
24352                // arm: this match is keyed on the FIRST token, and
24353                // `Token::Table` fell through to a tail that then
24354                // rejected what it got. `parse_table_shorthand` has
24355                // returned a desugared SelectStatement since the
24356                // shorthand landed — only the routing was missing.
24357                // Round 868 found this by putting the shorthand in a
24358                // subquery; every earlier check used a top-level form.
24359                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24360                // `SELECT * FROM t` this way and accepts it wherever a
24361                // SELECT goes, so the CTE body dispatch needs its own
24362                // arm: this match is keyed on the FIRST token, and
24363                // `Token::Table` fell through to a tail that rejected
24364                // what it got. `parse_table_shorthand` has returned a
24365                // desugared SelectStatement since the shorthand landed —
24366                // only the routing was missing, here and in the derived
24367                // table's second-token gate. Round 868 found both by
24368                // putting the shorthand in a subquery; every earlier
24369                // check had used a top-level form.
24370                Token::Table
24371                    if matches!(
24372                        self.tokens.get(self.pos + 1),
24373                        Some(Token::Ident(_) | Token::QuotedIdent(_))
24374                    ) =>
24375                {
24376                    let mut head = self.parse_table_shorthand()?;
24377                    self.parse_setop_chain_into(&mut head)?;
24378                    self.parse_select_tail_into(&mut head)?;
24379                    crate::ast::CteBody::Select(head)
24380                }
24381                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24382                // WITH t(a) AS (VALUES (1), (2)) … lowers through
24383                // the shared rows helper onto a Select body.
24384                Token::Values => {
24385                    self.advance(); // VALUES
24386                    let mut head = self.parse_values_rows_body()?;
24387                    // A VALUES seed can head a set-operation chain —
24388                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24389                    // SELECT n+1 FROM t …). Attach any trailing
24390                    // UNION / INTERSECT / EXCEPT peers so the
24391                    // recursive-CTE body parses like the SELECT seed.
24392                    self.parse_setop_chain_into(&mut head)?;
24393                    crate::ast::CteBody::Select(head)
24394                }
24395                Token::Insert => {
24396                    let inner = self.parse_one_statement()?;
24397                    let Statement::Insert(s) = inner else {
24398                        unreachable!("Token::Insert routes to Insert");
24399                    };
24400                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24401                }
24402                _ if is_update_kw => {
24403                    let inner = self.parse_one_statement()?;
24404                    let Statement::Update(s) = inner else {
24405                        return Err(
24406                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24407                        );
24408                    };
24409                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24410                }
24411                _ if is_delete_kw => {
24412                    let inner = self.parse_one_statement()?;
24413                    let Statement::Delete(s) = inner else {
24414                        return Err(
24415                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24416                        );
24417                    };
24418                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24419                }
24420                // v7.39 (round 149) — PG 17 allows MERGE as a
24421                // data-modifying CTE body.
24422                _ if is_merge_kw => {
24423                    let inner = self.parse_one_statement()?;
24424                    let Statement::Merge(s) = inner else {
24425                        return Err(
24426                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24427                        );
24428                    };
24429                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24430                }
24431                // v7.39 (round 151) — a CTE body may itself be
24432                // WITH-headed (PG grammar: PreparableStmt carries its
24433                // own with_clause). The nested statement keeps its own
24434                // ctes; the modifying-CTE-at-top-level rule is enforced
24435                // at execution.
24436                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24437                    self.advance(); // WITH
24438                    match self.parse_with_cte_then_select()? {
24439                        Statement::Select(s) => crate::ast::CteBody::Select(s),
24440                        Statement::Insert(s) => {
24441                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24442                        }
24443                        Statement::Update(s) => {
24444                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24445                        }
24446                        Statement::Delete(s) => {
24447                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24448                        }
24449                        Statement::Merge(s) => {
24450                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24451                        }
24452
24453                        other => {
24454                            return Err(self.err(format!(
24455                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24456                            )));
24457                        }
24458                    }
24459                }
24460                other => {
24461                    return Err(self.err(format!(
24462                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24463                    )));
24464                }
24465            };
24466            if !matches!(self.peek(), Token::RParen) {
24467                return Err(self.err(format!(
24468                    "expected ')' after CTE body, got {:?}",
24469                    self.peek()
24470                )));
24471            }
24472            self.advance();
24473            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24474            // CTE, desugared into extra body columns by the engine.
24475            let search = self.parse_cte_search_clause()?;
24476            let cycle = self.parse_cte_cycle_clause()?;
24477            let mut cte = crate::ast::Cte {
24478                name,
24479                body,
24480                recursive,
24481                column_overrides,
24482                search,
24483                cycle,
24484            };
24485            self.validate_recursive_cte(&cte)?;
24486            self.desugar_cte_search_cycle(&mut cte)?;
24487            ctes.push(cte);
24488            if matches!(self.peek(), Token::Comma) {
24489                self.advance();
24490                continue;
24491            }
24492            break;
24493        }
24494        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24495        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24496        // the parsed CTEs to whichever statement the body produces.
24497        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24498        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24499        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24500        match self.peek() {
24501            Token::Select => {
24502                let body_stmt = self.parse_select_stmt()?;
24503                let Statement::Select(mut body) = body_stmt else {
24504                    unreachable!()
24505                };
24506                body.ctes = ctes;
24507                Ok(Statement::Select(body))
24508            }
24509            Token::Insert => {
24510                let body_stmt = self.parse_one_statement()?;
24511                let Statement::Insert(mut body) = body_stmt else {
24512                    unreachable!()
24513                };
24514                body.ctes = ctes;
24515                Ok(Statement::Insert(body))
24516            }
24517            _ if outer_is_update => {
24518                let body_stmt = self.parse_one_statement()?;
24519                let Statement::Update(mut body) = body_stmt else {
24520                    return Err(self.err(format!("expected UPDATE after WITH clause")));
24521                };
24522                body.ctes = ctes;
24523                Ok(Statement::Update(body))
24524            }
24525            _ if outer_is_delete => {
24526                let body_stmt = self.parse_one_statement()?;
24527                let Statement::Delete(mut body) = body_stmt else {
24528                    return Err(self.err(format!("expected DELETE after WITH clause")));
24529                };
24530                body.ctes = ctes;
24531                Ok(Statement::Delete(body))
24532            }
24533            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24534            // WITH RECURSIVE is rejected with PG's exact message
24535            // (parse analysis, transformWithClause).
24536            _ if outer_is_merge => {
24537                if recursive {
24538                    return Err(self.err(String::from(
24539                        "WITH RECURSIVE is not supported for MERGE statement",
24540                    )));
24541                }
24542                let body_stmt = self.parse_one_statement()?;
24543                let Statement::Merge(mut body) = body_stmt else {
24544                    return Err(self.err(format!("expected MERGE after WITH clause")));
24545                };
24546                body.ctes = ctes;
24547                Ok(Statement::Merge(body))
24548            }
24549            other => Err(self.err(format!(
24550                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24551            ))),
24552        }
24553    }
24554
24555    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24556    /// already consumed the leading `EXISTS` ident via
24557    /// `self.advance()`.
24558    /// v7.13.0 — parse the rest of a `CASE … END` expression after
24559    /// the leading `CASE` ident has been consumed (mailrs round-5
24560    /// G9). Supports both the searched form
24561    /// (`CASE WHEN cond THEN val …`) and the simple form
24562    /// (`CASE operand WHEN val THEN val …`).
24563    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24564        // Disambiguate searched vs simple form: if the next token
24565        // is `WHEN`, we're in the searched form. Otherwise the
24566        // intervening expression is the operand.
24567        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24568            None
24569        } else {
24570            Some(Box::new(self.parse_expr(0)?))
24571        };
24572        let mut branches: Vec<(Expr, Expr)> = Vec::new();
24573        loop {
24574            match self.peek() {
24575                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24576                    self.advance();
24577                    let cond = self.parse_expr(0)?;
24578                    match self.peek() {
24579                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24580                            self.advance();
24581                        }
24582                        other => {
24583                            return Err(self.err(alloc::format!(
24584                                "expected THEN after CASE WHEN <expr>, got {other:?}"
24585                            )));
24586                        }
24587                    }
24588                    let value = self.parse_expr(0)?;
24589                    branches.push((cond, value));
24590                }
24591                _ => break,
24592            }
24593        }
24594        if branches.is_empty() {
24595            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24596        }
24597        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24598        {
24599            self.advance();
24600            Some(Box::new(self.parse_expr(0)?))
24601        } else {
24602            None
24603        };
24604        match self.peek() {
24605            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24606                self.advance();
24607            }
24608            other => {
24609                return Err(self.err(alloc::format!(
24610                    "expected END to close CASE expression, got {other:?}"
24611                )));
24612            }
24613        }
24614        Ok(Expr::Case {
24615            operand,
24616            branches,
24617            else_branch,
24618        })
24619    }
24620
24621    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24622    /// query-source position (EXISTS / IN / INSERT source / CTE body /
24623    /// view body). Caller consumed the WITH keyword. Only a SELECT
24624    /// outer is grammatical here; the data-modifying-CTE-at-top-level
24625    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
24626    /// maps correctly.
24627    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24628        let inner = self.parse_with_cte_then_select()?;
24629        match inner {
24630            Statement::Select(s) => Ok(s),
24631            other => Err(self.err(format!(
24632                "expected SELECT after WITH in a subquery, got {other:?}"
24633            ))),
24634        }
24635    }
24636
24637    /// True when the next token is the (unquoted) WITH keyword. WITH is
24638    /// reserved in PG, so a bare `with` can never be a column reference
24639    /// in these positions; a quoted `"with"` stays an identifier.
24640    fn peek_is_with_kw(&self) -> bool {
24641        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
24642    }
24643
24644    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
24645    /// `#[inline(never)]` keeps the large SelectStatement temporaries
24646    /// off parse_expr's recursive frame (the nesting-budget stack
24647    /// cliff — see the round-153 gate regression).
24648    #[inline(never)]
24649    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
24650        if self.peek_is_with_kw() {
24651            self.advance();
24652            self.parse_nested_with_select()
24653        } else {
24654            match self.parse_select_stmt()? {
24655                Statement::Select(s) => Ok(s),
24656                other => Err(self.err(alloc::format!(
24657                    "expected SELECT inside ANY/ALL, got {other:?}"
24658                ))),
24659            }
24660        }
24661    }
24662
24663    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
24664        if !matches!(self.peek(), Token::LParen) {
24665            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
24666        }
24667        self.advance();
24668        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
24669        let s = if self.peek_is_with_kw() {
24670            self.advance();
24671            self.parse_nested_with_select()?
24672        } else {
24673            let inner = self.parse_select_stmt()?;
24674            let Statement::Select(s) = inner else {
24675                unreachable!("parse_select_stmt returns Select")
24676            };
24677            s
24678        };
24679        if !matches!(self.peek(), Token::RParen) {
24680            return Err(self.err(format!(
24681                "expected ')' after EXISTS-subquery, got {:?}",
24682                self.peek()
24683            )));
24684        }
24685        self.advance();
24686        Ok(Expr::Exists {
24687            subquery: Box::new(s),
24688            negated,
24689        })
24690    }
24691
24692    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24693        self.advance(); // IN
24694        if !matches!(self.peek(), Token::LParen) {
24695            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
24696        }
24697        self.advance();
24698        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
24699        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
24700        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
24701            let s = if self.peek_is_with_kw() {
24702                self.advance();
24703                self.parse_nested_with_select()?
24704            } else {
24705                let inner = self.parse_select_stmt()?;
24706                let Statement::Select(s) = inner else {
24707                    unreachable!("parse_select_stmt always returns Statement::Select")
24708                };
24709                s
24710            };
24711            if !matches!(self.peek(), Token::RParen) {
24712                return Err(self.err(format!(
24713                    "expected ')' after IN-subquery, got {:?}",
24714                    self.peek()
24715                )));
24716            }
24717            self.advance();
24718            return Ok(Expr::InSubquery {
24719                expr: Box::new(expr),
24720                subquery: Box::new(s),
24721                negated,
24722            });
24723        }
24724        let mut elements = Vec::new();
24725        if !matches!(self.peek(), Token::RParen) {
24726            loop {
24727                elements.push(self.parse_expr(0)?);
24728                match self.peek() {
24729                    Token::Comma => {
24730                        self.advance();
24731                    }
24732                    Token::RParen => break,
24733                    other => {
24734                        return Err(
24735                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
24736                        );
24737                    }
24738                }
24739            }
24740        }
24741        self.advance(); // ')'
24742        // v7.30.2 (mailrs round-25) — flat InList node instead of a
24743        // left-deep OR-Eq chain: chain depth scaled with the element
24744        // count and overflowed the stack (eval + drop are recursive).
24745        if elements.is_empty() {
24746            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
24747        }
24748        Ok(Expr::InList {
24749            expr: Box::new(expr),
24750            list: elements,
24751            negated,
24752        })
24753    }
24754
24755    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
24756    /// already consumed by the caller. Elements must be numeric literals
24757    /// (with optional unary `-`); any compound expression is rejected at
24758    /// parse time so the runtime never needs to evaluate inside a vector.
24759    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
24760    /// has already consumed the `EXTRACT` token before calling us —
24761    /// we pick up at the opening `(`.
24762    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
24763    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
24764    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
24765    /// per-column OR-fold of
24766    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
24767    /// term)` so the existing FTS evaluator handles semantics.
24768    ///
24769    /// The mode modifier is accepted-and-ignored at v7.17 — all
24770    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
24771    /// mode operators (`+foo -bar`) would need their own parser
24772    /// (Phase 2.2c); customers who hit them today already get a
24773    /// correct lexeme-match against the bare term, only without
24774    /// the +/- precedence the customer asked for.
24775    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
24776        // Already at `MATCH`-consumed position; the dispatcher
24777        // confirmed the next token is `(`.
24778        if !matches!(self.peek(), Token::LParen) {
24779            return Err(self.err(alloc::format!(
24780                "expected '(' after MATCH, got {:?}",
24781                self.peek()
24782            )));
24783        }
24784        self.advance();
24785        let mut cols: Vec<Expr> = Vec::new();
24786        loop {
24787            cols.push(self.parse_expr(0)?);
24788            match self.peek() {
24789                Token::Comma => {
24790                    self.advance();
24791                }
24792                Token::RParen => break,
24793                other => {
24794                    return Err(self.err(alloc::format!(
24795                        "expected ',' or ')' in MATCH column list, got {other:?}"
24796                    )));
24797                }
24798            }
24799        }
24800        self.advance(); // ')'
24801        // Expect AGAINST.
24802        match self.peek() {
24803            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
24804                self.advance();
24805            }
24806            other => {
24807                return Err(self.err(alloc::format!(
24808                    "expected AGAINST after MATCH column list, got {other:?}"
24809                )));
24810            }
24811        }
24812        if !matches!(self.peek(), Token::LParen) {
24813            return Err(self.err(alloc::format!(
24814                "expected '(' after AGAINST, got {:?}",
24815                self.peek()
24816            )));
24817        }
24818        self.advance();
24819        // Read AGAINST's argument as a single primary token —
24820        // string literal, placeholder, or column-ref ident. We
24821        // can't call `parse_expr` / `parse_unary` here because
24822        // the postfix chain inside `parse_atom` would greedily
24823        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
24824        // and fail at "expected '(' after IN". Customers always
24825        // write a literal or bound parameter in AGAINST, so this
24826        // restriction is non-blocking; the error path explains
24827        // the limit if a more complex expression shows up.
24828        let term = match self.advance() {
24829            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
24830            Token::Placeholder(n) => Expr::Placeholder(n),
24831            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
24832                qualifier: None,
24833                name: s,
24834            }),
24835            other => {
24836                return Err(self.err(alloc::format!(
24837                    "MATCH ... AGAINST(<term>) expects a string literal, \
24838                     bound parameter, or column ref, got {other:?}"
24839                )));
24840            }
24841        };
24842        // Optional mode tail — accept-and-ignore at v7.17:
24843        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
24844        //   IN BOOLEAN MODE
24845        //   WITH QUERY EXPANSION
24846        loop {
24847            match self.peek() {
24848                // IN lexes as a reserved Token::In, not an ident,
24849                // so it gets its own arm.
24850                Token::In => {
24851                    self.advance();
24852                }
24853                Token::Ident(s) | Token::QuotedIdent(s)
24854                    if s.eq_ignore_ascii_case("natural")
24855                        || s.eq_ignore_ascii_case("language")
24856                        || s.eq_ignore_ascii_case("boolean")
24857                        || s.eq_ignore_ascii_case("mode")
24858                        || s.eq_ignore_ascii_case("with")
24859                        || s.eq_ignore_ascii_case("query")
24860                        || s.eq_ignore_ascii_case("expansion") =>
24861                {
24862                    self.advance();
24863                }
24864                _ => break,
24865            }
24866        }
24867        if !matches!(self.peek(), Token::RParen) {
24868            return Err(self.err(alloc::format!(
24869                "expected ')' to close AGAINST, got {:?}",
24870                self.peek()
24871            )));
24872        }
24873        self.advance();
24874        // Build per-column `to_tsvector('simple', col) @@
24875        // plainto_tsquery('simple', term)` and OR-fold.
24876        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
24877        let plainto = Expr::FunctionCall {
24878            name: String::from("plainto_tsquery"),
24879            args: alloc::vec![simple_lit(), term.clone()],
24880        };
24881        let mut folded: Option<Expr> = None;
24882        for col in cols {
24883            let to_tsv = Expr::FunctionCall {
24884                name: String::from("to_tsvector"),
24885                args: alloc::vec![simple_lit(), col],
24886            };
24887            let leaf = Expr::Binary {
24888                lhs: Box::new(to_tsv),
24889                op: crate::ast::BinOp::TsMatch,
24890                rhs: Box::new(plainto.clone()),
24891            };
24892            folded = Some(match folded {
24893                None => leaf,
24894                Some(prev) => Expr::Binary {
24895                    lhs: Box::new(prev),
24896                    op: crate::ast::BinOp::Or,
24897                    rhs: Box::new(leaf),
24898                },
24899            });
24900        }
24901        match folded {
24902            Some(e) => Ok(e),
24903            None => Err(self.err(String::from(
24904                "MATCH(...) AGAINST(...) requires at least one column",
24905            ))),
24906        }
24907    }
24908
24909    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
24910        if !matches!(self.peek(), Token::LParen) {
24911            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
24912        }
24913        self.advance();
24914        let field_name = self.expect_ident_like()?;
24915        let field = match field_name.to_ascii_lowercase().as_str() {
24916            // PG accepts the plural spellings (years/months/…/millenniums) as
24917            // aliases for the singular fields — its datetime unit table has both.
24918            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
24919            "year" | "years" => ExtractField::Year,
24920            "month" | "months" => ExtractField::Month,
24921            "day" | "days" => ExtractField::Day,
24922            "hour" | "hours" => ExtractField::Hour,
24923            "minute" | "minutes" => ExtractField::Minute,
24924            "second" | "seconds" => ExtractField::Second,
24925            "microsecond" | "microseconds" => ExtractField::Microsecond,
24926            "epoch" => ExtractField::Epoch,
24927            "dow" => ExtractField::Dow,
24928            "isodow" => ExtractField::Isodow,
24929            "doy" => ExtractField::Doy,
24930            "week" | "weeks" => ExtractField::Week,
24931            "isoyear" => ExtractField::Isoyear,
24932            "quarter" => ExtractField::Quarter,
24933            "decade" | "decades" => ExtractField::Decade,
24934            "century" | "centuries" => ExtractField::Century,
24935            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
24936            "julian" => ExtractField::Julian,
24937            "millisecond" | "milliseconds" => ExtractField::Millisecond,
24938            "timezone" => ExtractField::Timezone,
24939            "timezone_hour" => ExtractField::TimezoneHour,
24940            "timezone_minute" => ExtractField::TimezoneMinute,
24941            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
24942            // reports an unknown one with the source type (22023); carry the
24943            // raw name so eval can word it.
24944            other => ExtractField::Other(alloc::string::String::from(other)),
24945        };
24946        if !matches!(self.peek(), Token::From) {
24947            return Err(self.err(format!(
24948                "expected FROM after EXTRACT field, got {:?}",
24949                self.peek()
24950            )));
24951        }
24952        self.advance();
24953        let source = self.parse_expr(0)?;
24954        if !matches!(self.peek(), Token::RParen) {
24955            return Err(self.err(format!(
24956                "expected ')' to close EXTRACT, got {:?}",
24957                self.peek()
24958            )));
24959        }
24960        self.advance();
24961        Ok(Expr::Extract {
24962            field,
24963            source: Box::new(source),
24964        })
24965    }
24966
24967    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
24968    /// is already consumed; we expect a single string literal next and
24969    /// resolve it into `Literal::Interval` at parse time so the engine
24970    /// never has to re-tokenise inside the string.
24971    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
24972    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
24973    /// is the SQL-standard form and is left to the path below.
24974    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
24975        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
24976        let (offset, sign) = match self.peek() {
24977            Token::Minus => (1, "-"),
24978            _ => (0, ""),
24979        };
24980        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
24981            return None;
24982        };
24983        self.tokens
24984            .get(self.pos + offset + 1)
24985            .filter(|t| mysql_interval_unit(t).is_some())?;
24986        Some((alloc::format!("{sign}{n}"), offset + 1))
24987    }
24988
24989    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
24990    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
24991    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
24992    ///
24993    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
24994    /// this by parsing the group and then restoring `self.pos` — which could
24995    /// never have worked, because `advance()` DESTROYS the token it returns
24996    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
24997    /// inert only because both branches errored back then.
24998    fn interval_paren_is_quantity(&self) -> bool {
24999        let mut depth = 0usize;
25000        let mut saw_top_level_comma = false;
25001        let mut i = self.pos;
25002        while let Some(tok) = self.tokens.get(i) {
25003            match tok {
25004                Token::LParen => depth += 1,
25005                Token::RParen => {
25006                    depth = depth.saturating_sub(1);
25007                    if depth == 0 {
25008                        return !saw_top_level_comma
25009                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
25010                                .is_some();
25011                    }
25012                }
25013                // A comma directly inside the outermost parens means the
25014                // argument list of the INTERVAL() function.
25015                Token::Comma if depth == 1 => saw_top_level_comma = true,
25016                Token::Eof => return false,
25017                _ => {}
25018            }
25019            i += 1;
25020        }
25021        false
25022    }
25023
25024    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
25025        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
25026        // (the index of the last Ni ≤ N), distinct from the interval literal.
25027        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
25028        // is decided by a non-destructive lookahead (round 422) before either
25029        // branch consumes anything. MySQL only.
25030        if self.mysql_dialect
25031            && matches!(self.peek(), Token::LParen)
25032            && !self.interval_paren_is_quantity()
25033        {
25034            self.advance(); // (
25035            let mut args = Vec::new();
25036            if !matches!(self.peek(), Token::RParen) {
25037                loop {
25038                    args.push(self.parse_expr(0)?);
25039                    if matches!(self.peek(), Token::Comma) {
25040                        self.advance();
25041                        continue;
25042                    }
25043                    break;
25044                }
25045            }
25046            if !matches!(self.peek(), Token::RParen) {
25047                return Err(self.err(alloc::format!(
25048                    "expected ')' after INTERVAL() arguments, got {:?}",
25049                    self.peek()
25050                )));
25051            }
25052            self.advance(); // )
25053            return Ok(Expr::FunctionCall {
25054                name: alloc::string::String::from("interval"),
25055                args,
25056            });
25057        }
25058        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25059        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25060        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25061        // writes every date arithmetic there is, and it did not parse at
25062        // all. PG rejects the unquoted form outright (`syntax error at or
25063        // near "1"`, measured), so it is taken only in the MySQL dialect —
25064        // PG's own `INTERVAL '1' DAY` is untouched below.
25065        if self.mysql_dialect
25066            && let Some((text, consume)) = self.peek_unquoted_interval_count()
25067        {
25068            for _ in 0..consume {
25069                self.advance(); // the optional `-` and the number
25070            }
25071            let Some(unit) = mysql_interval_unit(self.peek()) else {
25072                return Err(self.err(alloc::format!(
25073                    "expected an interval unit after INTERVAL {text}, got {:?}",
25074                    self.peek()
25075                )));
25076            };
25077            self.advance(); // the unit
25078            let (months, days, micros) = scale_mysql_interval(&text, unit)
25079                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25080            return Ok(Expr::Literal(Literal::Interval {
25081                months,
25082                days,
25083                micros,
25084                // The canonical rendering, so Display round-trips into a
25085                // form both dialects read back.
25086                text: alloc::format!("{text} {unit}"),
25087            }));
25088        }
25089        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25090        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25091        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25092        // Those cannot fold into a compile-time `Literal::Interval`, so they
25093        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25094        // builtin, which builds the value at run time (and yields NULL for a
25095        // NULL quantity, as MariaDB does). The literal path above still folds
25096        // the constant case — it is cheaper and round-trips through Display.
25097        //
25098        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25099        // MySQL's quoted spelling) keep the qualifier path below.
25100        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25101            let qty = self.parse_expr(0)?;
25102            let Some(unit) = mysql_interval_unit(self.peek()) else {
25103                return Err(self.err(alloc::format!(
25104                    "expected an interval unit after INTERVAL <expr>, got {:?}",
25105                    self.peek()
25106                )));
25107            };
25108            self.advance(); // the unit
25109            return Ok(make_interval_call(qty, unit));
25110        }
25111        let tok = self.advance();
25112        let Token::String(text) = tok else {
25113            return Err(self.err(format!(
25114                "expected string literal after INTERVAL, got {tok:?}"
25115            )));
25116        };
25117        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25118        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25119        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25120        // bare number means and the leading/trailing precision.
25121        let field1 = interval_field_of(self.peek());
25122        let qualifier = if let Some(f1) = field1 {
25123            self.advance();
25124            let f2 = if matches!(self.peek(), Token::To) {
25125                self.advance();
25126                let Some(f) = interval_field_of(self.peek()) else {
25127                    return Err(self.err(format!(
25128                        "expected an interval field after TO, got {:?}",
25129                        self.peek()
25130                    )));
25131                };
25132                self.advance();
25133                Some(f)
25134            } else {
25135                None
25136            };
25137            Some((f1, f2))
25138        } else {
25139            None
25140        };
25141        let (months, days, micros) = match qualifier {
25142            Some(q) => interpret_qualified_interval(&text, q),
25143            None => parse_interval_text(&text),
25144        }
25145        .ok_or_else(|| ParseError {
25146            message: format!(
25147                "cannot parse INTERVAL {text:?}; \
25148                     expected `<n> <unit> [<n> <unit> ...]` with units \
25149                     microsecond[s], millisecond[s], second[s], minute[s], \
25150                     hour[s], day[s], week[s], month[s], year[s]"
25151            ),
25152            token_pos: self.consumed_pos(),
25153        })?;
25154        Ok(Expr::Literal(Literal::Interval {
25155            months,
25156            days,
25157            micros,
25158            text,
25159        }))
25160    }
25161
25162    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25163    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25164    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25165    /// than a pgvector literal.
25166    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25167        self.advance(); // consume `[`
25168        let mut items: Vec<Expr> = Vec::new();
25169        if !matches!(self.peek(), Token::RBracket) {
25170            loop {
25171                if matches!(self.peek(), Token::LBracket) {
25172                    items.push(self.parse_array_bracket_body()?);
25173                } else {
25174                    items.push(self.parse_expr(0)?);
25175                }
25176                match self.peek() {
25177                    Token::Comma => {
25178                        self.advance();
25179                    }
25180                    Token::RBracket => break,
25181                    other => {
25182                        return Err(self.err(alloc::format!(
25183                            "expected ',' or ']' in array literal, got {other:?}"
25184                        )));
25185                    }
25186                }
25187            }
25188        }
25189        self.advance(); // consume `]`
25190        Ok(Expr::Array(items))
25191    }
25192
25193    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25194        let mut elems = Vec::new();
25195        if matches!(self.peek(), Token::RBracket) {
25196            self.advance();
25197            return Ok(Expr::Literal(Literal::Vector(elems)));
25198        }
25199        loop {
25200            let e = self.parse_expr(0)?;
25201            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25202                message: format!("vector element must be a numeric literal, got {e:?}"),
25203                token_pos: self.pos,
25204            })?;
25205            elems.push(x);
25206            match self.peek() {
25207                Token::Comma => {
25208                    self.advance();
25209                }
25210                Token::RBracket => {
25211                    self.advance();
25212                    break;
25213                }
25214                other => {
25215                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25216                }
25217            }
25218        }
25219        Ok(Expr::Literal(Literal::Vector(elems)))
25220    }
25221
25222    /// Atom that started with an identifier: could be `t.col`, `col`, or
25223    /// `func(arg, ...)`. Detect each shape by looking at the next token.
25224    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25225    /// [, ...])`. Caller has already consumed `OVER`. Either clause
25226    /// is optional; an empty `()` is also legal (PG semantics).
25227    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25228    /// modifier between `name(args)` and `OVER (...)`. Default is
25229    /// `Respect`. Unrecognised idents leave the stream unchanged.
25230    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25231        let Token::Ident(s) = self.peek().clone() else {
25232            return NullTreatment::Respect;
25233        };
25234        let is_ignore = s.eq_ignore_ascii_case("ignore");
25235        let is_respect = s.eq_ignore_ascii_case("respect");
25236        if !is_ignore && !is_respect {
25237            return NullTreatment::Respect;
25238        }
25239        // Lookahead for NULLS — only consume both tokens together.
25240        // pos+1 must hold a "nulls" ident.
25241        if self.pos + 1 < self.tokens.len()
25242            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25243            && s2.eq_ignore_ascii_case("nulls")
25244        {
25245            self.advance();
25246            self.advance();
25247            return if is_ignore {
25248                NullTreatment::Ignore
25249            } else {
25250                NullTreatment::Respect
25251            };
25252        }
25253        NullTreatment::Respect
25254    }
25255
25256    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25257    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25258    /// (same shape as the `OVER` tail). Consumes the whole clause and
25259    /// returns the predicate; returns `None` when no `FILTER` follows.
25260    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25261        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25262            return Ok(None);
25263        };
25264        if !s.eq_ignore_ascii_case("filter") {
25265            return Ok(None);
25266        }
25267        self.advance(); // FILTER
25268        if !matches!(self.peek(), Token::LParen) {
25269            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25270        }
25271        self.advance(); // (
25272        if !matches!(self.peek(), Token::Where) {
25273            return Err(self.err(format!(
25274                "expected WHERE inside FILTER (...), got {:?}",
25275                self.peek()
25276            )));
25277        }
25278        self.advance(); // WHERE
25279        let cond = self.parse_expr(0)?;
25280        if !matches!(self.peek(), Token::RParen) {
25281            return Err(self.err(format!(
25282                "expected ')' to close FILTER (WHERE ...), got {:?}",
25283                self.peek()
25284            )));
25285        }
25286        self.advance(); // )
25287        Ok(Some(Box::new(cond)))
25288    }
25289
25290    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25291    /// the separator as the aggregate's second argument, which is the
25292    /// shape `string_agg` already takes. Returns whether one was there.
25293    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25294        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25295            return Ok(false);
25296        }
25297        self.advance();
25298        let Token::String(sep) = self.peek().clone() else {
25299            return Err(self.err(alloc::format!(
25300                "expected a string literal after SEPARATOR, got {:?}",
25301                self.peek()
25302            )));
25303        };
25304        self.advance();
25305        args.push(Expr::Literal(Literal::String(sep)));
25306        Ok(true)
25307    }
25308
25309    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25310    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25311    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25312    /// keys, or an empty vec when no `WITHIN GROUP` follows.
25313    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25314        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25315            return Ok(Vec::new());
25316        };
25317        if !s.eq_ignore_ascii_case("within") {
25318            return Ok(Vec::new());
25319        }
25320        self.advance(); // WITHIN
25321        if !matches!(self.peek(), Token::Group) {
25322            return Err(self.err(format!(
25323                "expected GROUP after WITHIN, got {:?}",
25324                self.peek()
25325            )));
25326        }
25327        self.advance(); // GROUP
25328        if !matches!(self.peek(), Token::LParen) {
25329            return Err(self.err(format!(
25330                "expected '(' after WITHIN GROUP, got {:?}",
25331                self.peek()
25332            )));
25333        }
25334        self.advance(); // (
25335        if !matches!(self.peek(), Token::Order) {
25336            return Err(self.err(format!(
25337                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25338                self.peek()
25339            )));
25340        }
25341        self.advance(); // ORDER
25342        if !self.peek_is_by() {
25343            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25344        }
25345        self.advance(); // BY
25346        let mut keys: Vec<OrderBy> = Vec::new();
25347        loop {
25348            // v7.39 (round 691) — save/restore, the discipline this parser
25349            // already uses around `pending_sample_preds`, so a subquery inside
25350            // a key neither inherits nor leaks the channel.
25351            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25352            let saved_coll = self.order_key_collation.take();
25353            let parsed = self.parse_expr(0);
25354            self.in_order_by_key = saved_flag;
25355            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25356            let expr = parsed?;
25357            let desc = if matches!(self.peek(), Token::Desc) {
25358                self.advance();
25359                true
25360            } else if matches!(self.peek(), Token::Asc) {
25361                self.advance();
25362                false
25363            } else {
25364                false
25365            };
25366            let nulls_first = self.parse_optional_nulls_placement()?;
25367            keys.push(OrderBy {
25368                expr,
25369                desc,
25370                nulls_first,
25371                collation,
25372            });
25373            if matches!(self.peek(), Token::Comma) {
25374                self.advance();
25375            } else {
25376                break;
25377            }
25378        }
25379        if !matches!(self.peek(), Token::RParen) {
25380            return Err(self.err(format!(
25381                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25382                self.peek()
25383            )));
25384        }
25385        self.advance(); // )
25386        Ok(keys)
25387    }
25388
25389    /// No frame clause is supported.
25390    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25391    fn parse_over_clause(
25392        &mut self,
25393    ) -> Result<
25394        (
25395            Vec<Expr>,
25396            Vec<(Expr, bool, Option<bool>)>,
25397            Option<WindowFrame>,
25398        ),
25399        ParseError,
25400    > {
25401        // `OVER w` — a named-window reference. The WINDOW clause
25402        // parses after the select list, so the name rides out as a
25403        // marker in partition_by; parse_bare_select substitutes the
25404        // definition once the clause is known.
25405        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25406            let name = w.clone();
25407            self.advance();
25408            return Ok((
25409                alloc::vec![Expr::Column(crate::ast::ColumnName {
25410                    qualifier: Some("__named_window__".to_string()),
25411                    name,
25412                })],
25413                Vec::new(),
25414                None,
25415            ));
25416        }
25417        if !matches!(self.peek(), Token::LParen) {
25418            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25419        }
25420        self.advance();
25421        let mut partition_by = Vec::new();
25422        let mut order_by = Vec::new();
25423        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25424        // window, refined in place. PG's rules (probed against 18.4) differ
25425        // from the bare `OVER w1` form, so the reference rides out under its
25426        // own marker and `substitute_named_windows` applies them. The base
25427        // name is any leading identifier that isn't a window-spec keyword.
25428        let base_window = match self.peek() {
25429            Token::Ident(s) | Token::QuotedIdent(s)
25430                if !s.eq_ignore_ascii_case("partition")
25431                    && !s.eq_ignore_ascii_case("rows")
25432                    && !s.eq_ignore_ascii_case("range")
25433                    && !s.eq_ignore_ascii_case("groups") =>
25434            {
25435                let n = s.clone();
25436                self.advance();
25437                Some(n)
25438            }
25439            _ => None,
25440        };
25441        // PARTITION BY ?
25442        // v7.37.6-B promoted PARTITION to a reserved keyword
25443        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25444        // `Token::Ident("partition")`. Accept both so older sources
25445        // and the new lexer surface land on the same path.
25446        let is_partition_kw = match self.peek() {
25447            Token::Partition => true,
25448            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25449            _ => false,
25450        };
25451        if is_partition_kw {
25452            self.advance();
25453            if !self.peek_is_by() {
25454                return Err(self.err(format!(
25455                    "expected BY after PARTITION, got {:?}",
25456                    self.peek()
25457                )));
25458            }
25459            self.advance();
25460            loop {
25461                partition_by.push(self.parse_expr(0)?);
25462                if matches!(self.peek(), Token::Comma) {
25463                    self.advance();
25464                    continue;
25465                }
25466                break;
25467            }
25468        }
25469        // ORDER BY ?
25470        if matches!(self.peek(), Token::Order) {
25471            self.advance();
25472            if !self.peek_is_by() {
25473                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25474            }
25475            self.advance();
25476            loop {
25477                let e = self.parse_expr(0)?;
25478                let desc = if matches!(self.peek(), Token::Desc) {
25479                    self.advance();
25480                    true
25481                } else if matches!(self.peek(), Token::Asc) {
25482                    self.advance();
25483                    false
25484                } else {
25485                    false
25486                };
25487                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25488                let nulls_first = self.parse_optional_nulls_placement()?;
25489                order_by.push((e, desc, nulls_first));
25490                if matches!(self.peek(), Token::Comma) {
25491                    self.advance();
25492                    continue;
25493                }
25494                break;
25495            }
25496        }
25497        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25498        // Both keywords come through the lexer as identifiers; match
25499        // case-insensitively.
25500        let mut frame: Option<WindowFrame> = None;
25501        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25502            let kind = if s.eq_ignore_ascii_case("rows") {
25503                Some(FrameKind::Rows)
25504            } else if s.eq_ignore_ascii_case("range") {
25505                Some(FrameKind::Range)
25506            } else if s.eq_ignore_ascii_case("groups") {
25507                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25508                Some(FrameKind::Groups)
25509            } else {
25510                None
25511            };
25512            if let Some(kind) = kind {
25513                self.advance();
25514                frame = Some(self.parse_frame_tail(kind)?);
25515            }
25516        }
25517        if !matches!(self.peek(), Token::RParen) {
25518            return Err(self.err(format!(
25519                "expected ')' to close OVER clause, got {:?}",
25520                self.peek()
25521            )));
25522        }
25523        self.advance();
25524        if let Some(base) = base_window {
25525            // A copy may refine but never override the base's partitioning
25526            // (PG rejects it outright, before looking the name up).
25527            if !partition_by.is_empty() {
25528                return Err(self.err(alloc::format!(
25529                    "cannot override PARTITION BY clause of window \"{base}\""
25530                )));
25531            }
25532            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25533                qualifier: Some("__named_window_ref__".to_string()),
25534                name: base,
25535            })];
25536        }
25537        Ok((partition_by, order_by, frame))
25538    }
25539
25540    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25541    /// or `RANGE` keyword was just consumed. Accepts both
25542    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25543    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25544    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25545    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25546        let (start, end) = if matches!(self.peek(), Token::Between) {
25547            self.advance();
25548            let start = self.parse_frame_bound()?;
25549            if !matches!(self.peek(), Token::And) {
25550                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25551            }
25552            self.advance();
25553            let end = self.parse_frame_bound()?;
25554            (start, Some(end))
25555        } else {
25556            (self.parse_frame_bound()?, None)
25557        };
25558        let exclude = self.parse_frame_exclusion()?;
25559        Ok(WindowFrame {
25560            kind,
25561            start,
25562            end,
25563            exclude,
25564        })
25565    }
25566
25567    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25568    /// after a frame spec. NO OTHERS is the default no-op.
25569    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25570        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25571            return Ok(FrameExclusion::NoOthers);
25572        }
25573        self.advance(); // EXCLUDE
25574        match self.peek() {
25575            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25576                self.advance();
25577                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25578                    return Err(self.err(format!(
25579                        "expected ROW after EXCLUDE CURRENT, got {:?}",
25580                        self.peek()
25581                    )));
25582                }
25583                self.advance();
25584                Ok(FrameExclusion::CurrentRow)
25585            }
25586            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25587            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25588            // Without this arm it fell to the catch-all, whose message
25589            // self-contradictingly listed GROUP as expected.
25590            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25591                self.advance();
25592                Ok(FrameExclusion::Group)
25593            }
25594            Token::Group => {
25595                self.advance();
25596                Ok(FrameExclusion::Group)
25597            }
25598            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25599                self.advance();
25600                Ok(FrameExclusion::Ties)
25601            }
25602            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25603                self.advance();
25604                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25605                    return Err(self.err(format!(
25606                        "expected OTHERS after EXCLUDE NO, got {:?}",
25607                        self.peek()
25608                    )));
25609                }
25610                self.advance();
25611                Ok(FrameExclusion::NoOthers)
25612            }
25613            other => Err(self.err(format!(
25614                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25615            ))),
25616        }
25617    }
25618
25619    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25620    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25621    /// `UNBOUNDED FOLLOWING`.
25622    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
25623        // Interval-typed offset for a value-based RANGE frame over a
25624        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
25625        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
25626        // PRECEDING`.
25627        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
25628            let dir = self.expect_ident_like()?;
25629            return if dir.eq_ignore_ascii_case("preceding") {
25630                Ok(FrameBound::IntervalPreceding {
25631                    months,
25632                    days,
25633                    micros,
25634                })
25635            } else if dir.eq_ignore_ascii_case("following") {
25636                Ok(FrameBound::IntervalFollowing {
25637                    months,
25638                    days,
25639                    micros,
25640                })
25641            } else {
25642                Err(self.err(format!(
25643                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
25644                )))
25645            };
25646        }
25647        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
25648        if let Token::Integer(n) = *self.peek() {
25649            self.advance();
25650            let n: u64 = u64::try_from(n).map_err(|_| {
25651                self.err(format!(
25652                    "invalid frame offset {n} — expected non-negative integer"
25653                ))
25654            })?;
25655            let dir = self.expect_ident_like()?;
25656            return if dir.eq_ignore_ascii_case("preceding") {
25657                Ok(FrameBound::OffsetPreceding(n))
25658            } else if dir.eq_ignore_ascii_case("following") {
25659                Ok(FrameBound::OffsetFollowing(n))
25660            } else {
25661                Err(self.err(format!(
25662                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
25663                )))
25664            };
25665        }
25666        let first = self.expect_ident_like()?;
25667        if first.eq_ignore_ascii_case("unbounded") {
25668            let dir = self.expect_ident_like()?;
25669            return if dir.eq_ignore_ascii_case("preceding") {
25670                Ok(FrameBound::UnboundedPreceding)
25671            } else if dir.eq_ignore_ascii_case("following") {
25672                Ok(FrameBound::UnboundedFollowing)
25673            } else {
25674                Err(self.err(format!(
25675                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
25676                )))
25677            };
25678        }
25679        if first.eq_ignore_ascii_case("current") {
25680            let row = self.expect_ident_like()?;
25681            if !row.eq_ignore_ascii_case("row") {
25682                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
25683            }
25684            return Ok(FrameBound::CurrentRow);
25685        }
25686        Err(self.err(format!(
25687            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
25688        )))
25689    }
25690
25691    /// Detect and consume a leading interval offset in a frame bound —
25692    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
25693    /// `(months, days, micros)`. Leaves the cursor on the trailing
25694    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
25695    /// when the next tokens are not an interval offset.
25696    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
25697        // Shape A — `INTERVAL '1 day'`.
25698        if matches!(self.peek(), Token::Interval) {
25699            self.advance(); // INTERVAL
25700            let atom = self.parse_interval_atom()?;
25701            if let Expr::Literal(Literal::Interval {
25702                months,
25703                days,
25704                micros,
25705                ..
25706            }) = atom
25707            {
25708                return Ok(Some((months, days, micros)));
25709            }
25710            return Err(self.err("expected an interval literal in frame offset".to_string()));
25711        }
25712        // Shape B — `'1 day'::interval`. Look ahead for the exact
25713        // string / `::` / interval-target triple before committing.
25714        if let Token::String(text) = self.peek() {
25715            let target_is_interval = match self.tokens.get(self.pos + 2) {
25716                Some(Token::Interval) => true,
25717                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
25718                _ => false,
25719            };
25720            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
25721                && target_is_interval;
25722            if is_cast {
25723                let text = text.clone();
25724                self.advance(); // string
25725                self.advance(); // ::
25726                self.advance(); // interval
25727                let parts = parse_interval_text(&text).ok_or_else(|| {
25728                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
25729                })?;
25730                return Ok(Some(parts));
25731            }
25732        }
25733        Ok(None)
25734    }
25735
25736    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
25737        // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
25738        // `_binary'z'`. All three were `ERROR 1064 syntax error` here
25739        // and all three answer the literal on MySQL 9.7.2.
25740        //
25741        // It is not only syntax, which is why it waited for
25742        // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
25743        // because `_binary` makes the comparison byte-wise, while
25744        // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
25745        // dropping the charset would have turned a hard error into a
25746        // silently wrong comparison — worse than the error it replaced.
25747        //
25748        // An UNKNOWN charset is NOT an introducer: MySQL answers
25749        // `Unknown column '_nosuch'`, because it parses as a column
25750        // reference followed by a string. So the table decides, and it
25751        // is the same table `SET NAMES` reads.
25752        //
25753        // A space is allowed between the two (`_utf8mb4 'x'`), which
25754        // falls out of asking the token stream rather than the bytes.
25755        if self.mysql_dialect
25756            && let Token::String(_) = self.peek()
25757        {
25758            let lower = first.to_ascii_lowercase();
25759            let charset = if lower == "n" {
25760                // `N'…'` is the national character set, which MySQL
25761                // documents as utf8 — utf8mb3 in 9.7.2's spelling.
25762                //
25763                // utf8mb3 and utf8mb4 both fold case in their default
25764                // collations, so nothing SPG can be asked distinguishes
25765                // the two here: an ablation swapping this to utf8mb4
25766                // reddens no pin. Recorded rather than implied — the
25767                // spelling follows MySQL's documentation, not a
25768                // measurement.
25769                Some("utf8mb3")
25770            } else {
25771                // No filter here: the lookup below IS the test for
25772                // "is this a charset". An ablation that removed a filter
25773                // in this spot reddened nothing, which is how the two
25774                // were found to be one check written twice.
25775                lower.strip_prefix('_')
25776            };
25777            if let Some(cs) = charset
25778                && let Some(collation) = crate::charset::charset_default_collation(cs)
25779            {
25780                let Token::String(body) = self.advance() else {
25781                    unreachable!("peeked a string");
25782                };
25783                return Ok(Expr::Collate {
25784                    expr: Box::new(Expr::Literal(Literal::String(body))),
25785                    collation: String::from(collation),
25786                });
25787            }
25788        }
25789        if matches!(self.peek(), Token::Dot) {
25790            self.advance();
25791            let name = self.expect_ident_like()?;
25792            // v7.14.0 — schema-qualified function call
25793            // `<schema>.<fn>(args)`. PG dumps emit
25794            // `pg_catalog.set_config(...)` in the preamble. SPG
25795            // is single-namespace: drop the schema prefix and
25796            // route the dispatch on the bare function name.
25797            if matches!(self.peek(), Token::LParen) {
25798                return self.finish_ident_atom(name);
25799            }
25800            return Ok(Expr::Column(ColumnName {
25801                qualifier: Some(first),
25802                name,
25803            }));
25804        }
25805        if matches!(self.peek(), Token::LParen) {
25806            self.advance();
25807            // `COUNT(*)` — special-cased here because `*` isn't a normal
25808            // expression token. Lower-case match on `first` since the lexer
25809            // folds identifiers.
25810            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
25811                self.advance();
25812                if !matches!(self.peek(), Token::RParen) {
25813                    return Err(self.err(format!(
25814                        "expected ')' after COUNT(*), got {:?}",
25815                        self.peek()
25816                    )));
25817                }
25818                self.advance();
25819                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
25820                let filter = self.parse_filter_clause()?;
25821                // v4.12: COUNT(*) OVER (...) — same window tail.
25822                let null_treatment = self.parse_null_treatment_modifier();
25823                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
25824                    && s.eq_ignore_ascii_case("over")
25825                {
25826                    self.advance();
25827                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
25828                    return Ok(Expr::WindowFunction {
25829                        name: "count_star".into(),
25830                        args: Vec::new(),
25831                        partition_by,
25832                        order_by,
25833                        frame,
25834                        null_treatment,
25835                        filter,
25836                    });
25837                }
25838                if let Some(filter) = filter {
25839                    return Ok(Expr::AggregateOrdered {
25840                        call: Box::new(Expr::FunctionCall {
25841                            name: "count_star".into(),
25842                            args: Vec::new(),
25843                        }),
25844                        order_by: Vec::new(),
25845                        distinct: false,
25846                        filter: Some(filter),
25847                    });
25848                }
25849                return Ok(Expr::FunctionCall {
25850                    name: "count_star".into(),
25851                    args: Vec::new(),
25852                });
25853            }
25854            // Function call. PG-style: zero-or-more comma-separated args.
25855            let mut args = Vec::new();
25856            // v7.38 (read01, T14) — named-argument notation `argname => value`.
25857            // Names are collected in lock-step with `args` and resolved to
25858            // positional order after the loop (the AST stays positional).
25859            let mut arg_names: Vec<Option<String>> = Vec::new();
25860            let mut agg_order_by: Vec<OrderBy> = Vec::new();
25861            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
25862            // seen, so the value arguments before it can be folded.
25863            let mut saw_separator = false;
25864            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
25865            // v7.32 (round-29) — accept the dual `ALL` quantifier too
25866            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
25867            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
25868                self.advance();
25869                true
25870            } else if matches!(self.peek(), Token::All) {
25871                self.advance();
25872                false
25873            } else {
25874                false
25875            };
25876            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
25877            // TIMESTAMPDIFF take a bare unit keyword as the first
25878            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
25879            // bare type keyword (DATE / TIME / DATETIME); lower them
25880            // onto string literals so the evaluator sees plain text.
25881            if ((first.eq_ignore_ascii_case("timestampadd")
25882                || first.eq_ignore_ascii_case("timestampdiff"))
25883                && matches!(self.peek(), Token::Ident(u) if matches!(
25884                    u.to_ascii_lowercase().as_str(),
25885                    "microsecond" | "second" | "minute" | "hour" | "day"
25886                        | "week" | "month" | "quarter" | "year"
25887                )))
25888                || (first.eq_ignore_ascii_case("get_format")
25889                    && matches!(self.peek(), Token::Ident(u) if matches!(
25890                        u.to_ascii_lowercase().as_str(),
25891                        "date" | "time" | "datetime" | "timestamp"
25892                    )))
25893            {
25894                if let Token::Ident(u) = self.peek() {
25895                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
25896                }
25897                self.advance();
25898                if matches!(self.peek(), Token::Comma) {
25899                    self.advance();
25900                }
25901            }
25902            // `ROW(a, b, …)` keyword constructor. Followed by a
25903            // comparison operator or [NOT] IN it joins the paren
25904            // row-constructor machinery (fieldwise parse-time
25905            // expansion); bare, it stays a `row` call the evaluator
25906            // renders as PG record text.
25907            if first.eq_ignore_ascii_case("row") {
25908                let mut row_items = Vec::new();
25909                if !matches!(self.peek(), Token::RParen) {
25910                    loop {
25911                        row_items.push(self.parse_expr(0)?);
25912                        match self.peek() {
25913                            Token::Comma => {
25914                                self.advance();
25915                            }
25916                            Token::RParen => break,
25917                            other => {
25918                                return Err(self.err(format!(
25919                                    "expected ',' or ')' in ROW(...), got {other:?}"
25920                                )));
25921                            }
25922                        }
25923                    }
25924                }
25925                self.advance(); // ')'
25926                let comparison_follows = matches!(
25927                    self.peek(),
25928                    Token::Eq
25929                        | Token::NotEq
25930                        | Token::Lt
25931                        | Token::LtEq
25932                        | Token::Gt
25933                        | Token::GtEq
25934                        | Token::In
25935                ) || (matches!(self.peek(), Token::Not)
25936                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
25937                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
25938                if comparison_follows && !row_items.is_empty() {
25939                    return self.parse_row_comparison_tail(row_items);
25940                }
25941                return Ok(Expr::FunctionCall {
25942                    name: String::from("row"),
25943                    args: row_items,
25944                });
25945            }
25946            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
25947            // the parse-mode keyword introduces the source text. SPG
25948            // carries XML as text, so both modes lower to __xmlparse(expr)
25949            // which validates well-formedness and returns Value::Xml.
25950            if first.eq_ignore_ascii_case("xmlparse")
25951                && matches!(self.peek(), Token::Ident(kw)
25952                    if kw.eq_ignore_ascii_case("document")
25953                        || kw.eq_ignore_ascii_case("content"))
25954            {
25955                let mode = match self.advance() {
25956                    Token::Ident(kw) => kw.to_ascii_lowercase(),
25957                    _ => unreachable!("peeked an ident"),
25958                };
25959                let src = self.parse_expr(0)?;
25960                if !matches!(self.peek(), Token::RParen) {
25961                    return Err(self.err(format!(
25962                        "expected ')' to close XMLPARSE, got {:?}",
25963                        self.peek()
25964                    )));
25965                }
25966                self.advance();
25967                return Ok(Expr::FunctionCall {
25968                    name: String::from("__xmlparse"),
25969                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
25970                });
25971            }
25972            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
25973            // keyword introduces the element name (a bare or quoted
25974            // identifier), then optional content expressions. Lower to a
25975            // plain `xmlelement(name_text, content …)` call.
25976            if first.eq_ignore_ascii_case("xmlelement")
25977                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
25978            {
25979                self.advance(); // consume NAME
25980                let elem_name = match self.peek().clone() {
25981                    Token::Ident(n) | Token::QuotedIdent(n) => {
25982                        self.advance();
25983                        n
25984                    }
25985                    other => {
25986                        return Err(self.err(format!(
25987                            "expected element name after XMLELEMENT NAME, got {other:?}"
25988                        )));
25989                    }
25990                };
25991                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
25992                while matches!(self.peek(), Token::Comma) {
25993                    self.advance();
25994                    args.push(self.parse_expr(0)?);
25995                }
25996                if !matches!(self.peek(), Token::RParen) {
25997                    return Err(self.err(format!(
25998                        "expected ')' to close XMLELEMENT, got {:?}",
25999                        self.peek()
26000                    )));
26001                }
26002                self.advance();
26003                return Ok(Expr::FunctionCall {
26004                    name: String::from("xmlelement"),
26005                    args,
26006                });
26007            }
26008            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
26009            // becomes a `<name>value</name>` element; a bare column infers its
26010            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
26011            // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
26012            // `CONVERT(expr USING cs)` was a syntax error at USING, and
26013            // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
26014            // `convert(bytea, src, dest)` and answered `column "char" does
26015            // not exist`. Both are casts in MySQL: measured on 9.7.2,
26016            // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
26017            // both 'A', and `CONVERT(123, CHAR)` is '123'.
26018            //
26019            // The charset is checked against the same table the introducers
26020            // use, so an unknown one is refused rather than quietly ignored.
26021            if self.mysql_dialect
26022                && first.eq_ignore_ascii_case("convert")
26023                && !matches!(self.peek(), Token::RParen)
26024            {
26025                let save = self.pos;
26026                let inner = self.parse_expr(0)?;
26027                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
26028                    self.advance();
26029                    let cs = match self.peek().clone() {
26030                        Token::Ident(n) | Token::QuotedIdent(n) => {
26031                            self.advance();
26032                            n
26033                        }
26034                        other => {
26035                            return Err(self.err(alloc::format!(
26036                                "expected a charset after USING, got {other:?}"
26037                            )));
26038                        }
26039                    };
26040                    let lc = cs.to_ascii_lowercase();
26041                    if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26042                        return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26043                    }
26044                    if !matches!(self.peek(), Token::RParen) {
26045                        return Err(self.err(alloc::format!(
26046                            "expected ')' after CONVERT … USING, got {:?}",
26047                            self.peek()
26048                        )));
26049                    }
26050                    self.advance();
26051                    let target = if lc == "binary" {
26052                        CastTarget::Named("binary".to_string())
26053                    } else {
26054                        CastTarget::Text
26055                    };
26056                    return self.finish_postfix_casts(Expr::Cast {
26057                        expr: alloc::boxed::Box::new(inner),
26058                        target,
26059                    });
26060                }
26061                if matches!(self.peek(), Token::Comma) {
26062                    self.advance();
26063                    // A type name here is MySQL's cast form; anything else
26064                    // (three string arguments) is PostgreSQL's `convert`,
26065                    // which keeps its own path.
26066                    if let Ok(target) = self.parse_cast_target()
26067                        && matches!(self.peek(), Token::RParen)
26068                    {
26069                        self.advance();
26070                        return self.finish_postfix_casts(Expr::Cast {
26071                            expr: alloc::boxed::Box::new(inner),
26072                            target,
26073                        });
26074                    }
26075                }
26076                self.pos = save;
26077            }
26078            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26079                let mut args: Vec<Expr> = Vec::new();
26080                loop {
26081                    let val = self.parse_expr(0)?;
26082                    let name = if matches!(self.peek(), Token::As) {
26083                        self.advance();
26084                        match self.peek().clone() {
26085                            Token::Ident(n) | Token::QuotedIdent(n) => {
26086                                self.advance();
26087                                n
26088                            }
26089                            other => {
26090                                return Err(self.err(format!(
26091                                    "expected name after AS in XMLFOREST, got {other:?}"
26092                                )));
26093                            }
26094                        }
26095                    } else if let Expr::Column(c) = &val {
26096                        c.name.clone()
26097                    } else {
26098                        return Err(
26099                            self.err("XMLFOREST element without a column name needs AS".into())
26100                        );
26101                    };
26102                    args.push(Expr::Literal(Literal::String(name)));
26103                    args.push(val);
26104                    if matches!(self.peek(), Token::Comma) {
26105                        self.advance();
26106                    } else {
26107                        break;
26108                    }
26109                }
26110                if !matches!(self.peek(), Token::RParen) {
26111                    return Err(self.err(format!(
26112                        "expected ')' to close XMLFOREST, got {:?}",
26113                        self.peek()
26114                    )));
26115                }
26116                self.advance();
26117                return Ok(Expr::FunctionCall {
26118                    name: String::from("xmlforest"),
26119                    args,
26120                });
26121            }
26122            // SQL-standard `POSITION(sub IN str)` — lowers onto
26123            // strpos(str, sub). IN is the argument separator here,
26124            // so the needle parses with the IN-tail suppressed.
26125            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26126                let saved = self.suppress_in_tail;
26127                self.suppress_in_tail = true;
26128                let needle = self.parse_expr(0);
26129                self.suppress_in_tail = saved;
26130                let needle = needle?;
26131                if matches!(self.peek(), Token::In) {
26132                    self.advance();
26133                    let haystack = self.parse_expr(0)?;
26134                    if !matches!(self.peek(), Token::RParen) {
26135                        return Err(self.err(format!(
26136                            "expected ')' to close POSITION, got {:?}",
26137                            self.peek()
26138                        )));
26139                    }
26140                    self.advance();
26141                    return Ok(Expr::FunctionCall {
26142                        name: String::from("strpos"),
26143                        args: alloc::vec![haystack, needle],
26144                    });
26145                }
26146                // position(sub, str) comma form (incl. bytea) —
26147                // hand the parsed first arg to the generic list.
26148                args.push(needle);
26149                if matches!(self.peek(), Token::Comma) {
26150                    self.advance();
26151                }
26152            }
26153            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26154            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26155            // plain comma forms TRIM(str) / TRIM(str, chars) keep
26156            // riding the generic argument list below.
26157            if first.eq_ignore_ascii_case("trim") {
26158                let mode = match self.peek() {
26159                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26160                        self.advance();
26161                        Some("btrim")
26162                    }
26163                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26164                        self.advance();
26165                        Some("ltrim")
26166                    }
26167                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26168                        self.advance();
26169                        Some("rtrim")
26170                    }
26171                    _ => None,
26172                };
26173                if mode.is_some() || matches!(self.peek(), Token::From) {
26174                    // TRIM([mode] FROM str) — no strip-chars.
26175                    let chars = if matches!(self.peek(), Token::From) {
26176                        None
26177                    } else {
26178                        Some(self.parse_expr(0)?)
26179                    };
26180                    if !matches!(self.peek(), Token::From) {
26181                        return Err(self.err(format!(
26182                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26183                            self.peek()
26184                        )));
26185                    }
26186                    self.advance();
26187                    let target = self.parse_expr(0)?;
26188                    if !matches!(self.peek(), Token::RParen) {
26189                        return Err(
26190                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26191                        );
26192                    }
26193                    self.advance();
26194                    let mut trim_args = alloc::vec![target];
26195                    if let Some(c) = chars {
26196                        trim_args.push(c);
26197                    }
26198                    return Ok(Expr::FunctionCall {
26199                        name: String::from(mode.unwrap_or("btrim")),
26200                        args: trim_args,
26201                    });
26202                }
26203            }
26204            if !matches!(self.peek(), Token::RParen) {
26205                loop {
26206                    // v7.38 (read01, T14) — `argname => value` names this arg.
26207                    // v7.39 (read01 round 77) — `argname := value` is the same
26208                    // thing, and it is the spelling PG's own docs lead with. It
26209                    // was simply never lexed here, so every `f(x := 1)` died in
26210                    // the parser regardless of what `f` was.
26211                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26212                        (
26213                            Token::Ident(n) | Token::QuotedIdent(n),
26214                            Some(Token::FatArrow | Token::ColonEq),
26215                        ) => {
26216                            let name = n.clone();
26217                            self.advance(); // name
26218                            self.advance(); // => / :=
26219                            Some(name)
26220                        }
26221                        _ => None,
26222                    };
26223                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26224                    // array's elements into a variadic call's trailing args
26225                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26226                    // reserved, so it arrives as a bare ident before the arg.
26227                    let is_variadic = this_name.is_none()
26228                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26229                    if is_variadic {
26230                        self.advance();
26231                    }
26232                    let arg = self.parse_expr(0)?;
26233                    args.push(match &this_name {
26234                        // The callee's parameter names decide the slot, and a
26235                        // user function's live in the catalog. Carry the name
26236                        // to eval rather than guessing here.
26237                        Some(n) => Expr::NamedArg {
26238                            name: n.clone(),
26239                            expr: Box::new(arg),
26240                        },
26241                        None if is_variadic => Expr::Variadic(Box::new(arg)),
26242                        None => arg,
26243                    });
26244                    arg_names.push(this_name);
26245                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
26246                    // The `::` cast already worked; this lowers the
26247                    // function form onto the same Expr::Cast node.
26248                    if first.eq_ignore_ascii_case("cast")
26249                        && args.len() == 1
26250                        && matches!(self.peek(), Token::As)
26251                    {
26252                        self.advance();
26253                        let target = self.parse_cast_target()?;
26254                        if !matches!(self.peek(), Token::RParen) {
26255                            return Err(self.err(format!(
26256                                "expected ')' to close CAST, got {:?}",
26257                                self.peek()
26258                            )));
26259                        }
26260                        self.advance();
26261                        return Ok(Expr::Cast {
26262                            expr: Box::new(args.pop().expect("one arg")),
26263                            target,
26264                        });
26265                    }
26266                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26267                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26268                    // keywords; SPG's lexer makes them plain idents (so they'd be
26269                    // read as column refs). Lower the keyword to the string form
26270                    // the evaluator already accepts.
26271                    if first.eq_ignore_ascii_case("normalize")
26272                        && args.len() == 1
26273                        && matches!(self.peek(), Token::Comma)
26274                    {
26275                        let form = match self.tokens.get(self.pos + 1) {
26276                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26277                                let up = f.to_ascii_uppercase();
26278                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26279                            }
26280                            _ => None,
26281                        };
26282                        if let Some(up) = form {
26283                            self.advance(); // comma
26284                            self.advance(); // form keyword
26285                            args.push(Expr::Literal(Literal::String(up)));
26286                        }
26287                    }
26288                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26289                    // form. Desugars to the comma-list shape evaluator already
26290                    // handles. Triggered after the first arg when the function
26291                    // name is substring / substr and the next token is FROM
26292                    // (a reserved keyword in PG; SPG also reserves it).
26293                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26294                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26295                    // internal __substring_similar(str, pat, esc) call.
26296                    if (first.eq_ignore_ascii_case("substring")
26297                        || first.eq_ignore_ascii_case("substr"))
26298                        && args.len() == 1
26299                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26300                    {
26301                        self.advance(); // SIMILAR
26302                        let pattern = self.parse_expr(0)?;
26303                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26304                        {
26305                            return Err(self.err(format!(
26306                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26307                                self.peek()
26308                            )));
26309                        }
26310                        self.advance(); // ESCAPE
26311                        let esc = self.parse_expr(0)?;
26312                        if !matches!(self.peek(), Token::RParen) {
26313                            return Err(self.err(format!(
26314                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
26315                                self.peek()
26316                            )));
26317                        }
26318                        self.advance();
26319                        args.push(pattern);
26320                        args.push(esc);
26321                        return Ok(Expr::FunctionCall {
26322                            name: "__substring_similar".to_string(),
26323                            args,
26324                        });
26325                    }
26326                    if (first.eq_ignore_ascii_case("substring")
26327                        || first.eq_ignore_ascii_case("substr"))
26328                        && args.len() == 1
26329                        && matches!(self.peek(), Token::From | Token::For)
26330                    {
26331                        // `substring(str FROM pos [FOR len])`, or the FOR-only
26332                        // `substring(str FOR len)` which PG treats as FROM 1.
26333                        if matches!(self.peek(), Token::From) {
26334                            self.advance();
26335                            let start = self.parse_expr(0)?;
26336                            args.push(start);
26337                        } else {
26338                            args.push(Expr::Literal(Literal::Integer(1)));
26339                        }
26340                        if matches!(self.peek(), Token::For) {
26341                            self.advance();
26342                            let length = self.parse_expr(0)?;
26343                            args.push(length);
26344                        }
26345                        if !matches!(self.peek(), Token::RParen) {
26346                            return Err(self.err(format!(
26347                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26348                                self.peek()
26349                            )));
26350                        }
26351                        self.advance();
26352                        return Ok(Expr::FunctionCall {
26353                            name: first.to_ascii_lowercase(),
26354                            args,
26355                        });
26356                    }
26357                    // PG `overlay(str PLACING repl FROM n [FOR len])`
26358                    // syntactic form. Desugars to the `overlay(str,
26359                    // repl, n[, len])` comma-list shape the evaluator
26360                    // already implements. `PLACING` is not a reserved
26361                    // token in SPG, so it arrives as a bare Ident.
26362                    if first.eq_ignore_ascii_case("overlay")
26363                        && args.len() == 1
26364                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26365                    {
26366                        self.advance(); // consume PLACING
26367                        args.push(self.parse_expr(0)?); // replacement
26368                        if !matches!(self.peek(), Token::From) {
26369                            return Err(self.err(format!(
26370                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26371                                self.peek()
26372                            )));
26373                        }
26374                        self.advance();
26375                        args.push(self.parse_expr(0)?); // start position
26376                        if matches!(self.peek(), Token::For) {
26377                            self.advance();
26378                            args.push(self.parse_expr(0)?); // length
26379                        }
26380                        if !matches!(self.peek(), Token::RParen) {
26381                            return Err(self.err(format!(
26382                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26383                                self.peek()
26384                            )));
26385                        }
26386                        self.advance();
26387                        return Ok(Expr::FunctionCall {
26388                            name: String::from("overlay"),
26389                            args,
26390                        });
26391                    }
26392                    // `TRIM(chars FROM str)` — the keyword-less
26393                    // spelling lands here after the chars parse
26394                    // (the keyword forms return earlier).
26395                    if first.eq_ignore_ascii_case("trim")
26396                        && args.len() == 1
26397                        && matches!(self.peek(), Token::From)
26398                    {
26399                        self.advance();
26400                        let target = self.parse_expr(0)?;
26401                        if !matches!(self.peek(), Token::RParen) {
26402                            return Err(self.err(format!(
26403                                "expected ')' to close TRIM(chars FROM str), got {:?}",
26404                                self.peek()
26405                            )));
26406                        }
26407                        self.advance();
26408                        let chars = args.pop().expect("one arg");
26409                        return Ok(Expr::FunctionCall {
26410                            name: String::from("btrim"),
26411                            args: alloc::vec![target, chars],
26412                        });
26413                    }
26414                    // v7.24 (round-16 A) — aggregate-internal
26415                    // ordering: `array_agg(x ORDER BY y DESC NULLS
26416                    // LAST)`. Keys close the argument list.
26417                    if matches!(self.peek(), Token::Order) {
26418                        self.advance();
26419                        if !self.peek_is_by() {
26420                            return Err(self.err(format!(
26421                                "expected BY after ORDER in aggregate args, got {:?}",
26422                                self.peek()
26423                            )));
26424                        }
26425                        self.advance();
26426                        loop {
26427                            // v7.39 (round 691) — save/restore, the discipline this parser
26428                            // already uses around `pending_sample_preds`, so a subquery inside
26429                            // a key neither inherits nor leaks the channel.
26430                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26431                            let saved_coll = self.order_key_collation.take();
26432                            let parsed = self.parse_expr(0);
26433                            self.in_order_by_key = saved_flag;
26434                            let collation =
26435                                core::mem::replace(&mut self.order_key_collation, saved_coll);
26436                            let expr = parsed?;
26437                            let desc = if matches!(self.peek(), Token::Desc) {
26438                                self.advance();
26439                                true
26440                            } else if matches!(self.peek(), Token::Asc) {
26441                                self.advance();
26442                                false
26443                            } else {
26444                                false
26445                            };
26446                            let nulls_first = self.parse_optional_nulls_placement()?;
26447                            agg_order_by.push(OrderBy {
26448                                expr,
26449                                desc,
26450                                nulls_first,
26451                                collation,
26452                            });
26453                            if matches!(self.peek(), Token::Comma) {
26454                                self.advance();
26455                            } else {
26456                                break;
26457                            }
26458                        }
26459                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26460                        // follow the ORDER BY inside GROUP_CONCAT.
26461                        if self.consume_group_concat_separator(&mut args)? {
26462                            saw_separator = true;
26463                        }
26464                        if !matches!(self.peek(), Token::RParen) {
26465                            return Err(self.err(format!(
26466                                "expected ')' after aggregate ORDER BY, got {:?}",
26467                                self.peek()
26468                            )));
26469                        }
26470                        break;
26471                    }
26472                    // v7.39 (round 354, M12) — …or directly after the
26473                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26474                    // own spelling of what PG passes as string_agg's second
26475                    // argument; it was a parse error, so every MySQL query
26476                    // that names its own separator failed outright.
26477                    if self.consume_group_concat_separator(&mut args)? {
26478                        saw_separator = true;
26479                        break;
26480                    }
26481                    match self.peek() {
26482                        Token::Comma => {
26483                            self.advance();
26484                        }
26485                        Token::RParen => break,
26486                        other => {
26487                            return Err(self.err(format!(
26488                                "expected ',' or ')' in function args, got {other:?}"
26489                            )));
26490                        }
26491                    }
26492                }
26493            }
26494            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26495            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26496            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26497            // meaning a separator — that is what the explicit SEPARATOR
26498            // tail is for. Fold them into one `concat(...)` so the
26499            // aggregate keeps its single value argument.
26500            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26501                let values = args.len() - usize::from(saw_separator);
26502                if values > 1 {
26503                    let sep_arg = if saw_separator { args.pop() } else { None };
26504                    let folded = Expr::FunctionCall {
26505                        name: "concat".to_string(),
26506                        args: core::mem::take(&mut args),
26507                    };
26508                    args.push(folded);
26509                    if let Some(sep) = sep_arg {
26510                        args.push(sep);
26511                    }
26512                }
26513            }
26514            self.advance(); // consume ')'
26515            // v7.39 (read01 round 77) — named arguments are NOT reordered here
26516            // any more. The parser has no catalog, so it could only ever resolve
26517            // the handful of `make_*` builtins whose parameter names were baked
26518            // into a table right here — every user function got
26519            // "does not support named arguments", though the catalog has been
26520            // storing its parameter names all along. Reordering happens in eval,
26521            // in one place, for builtins and user functions alike.
26522            // v7.32 (round-29) — ordered-set aggregate tail
26523            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
26524            // (percentile_cont / percentile_disc / mode). The sort spec
26525            // lands in the same `order_by` slot a decorated aggregate
26526            // uses; the executor dispatches on the function name. WITHIN
26527            // GROUP and an intra-argument ORDER BY are mutually
26528            // exclusive (PG rejects both).
26529            let within_group_order = self.parse_within_group_clause()?;
26530            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
26531                return Err(self.err(
26532                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
26533                        .into(),
26534                ));
26535            }
26536            let within_group_seen = !within_group_order.is_empty();
26537            let agg_order_by = if within_group_order.is_empty() {
26538                agg_order_by
26539            } else {
26540                within_group_order
26541            };
26542            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
26543            let filter = self.parse_filter_clause()?;
26544            // v4.12: window-function tail — `name(args) OVER (...)`.
26545            // Promotes the just-parsed FunctionCall into a
26546            // WindowFunction node carrying partition + order.
26547            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
26548            // / `RESPECT NULLS OVER (...)` between the closing paren
26549            // and `OVER`.
26550            let null_treatment = self.parse_null_treatment_modifier();
26551            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26552                && s.eq_ignore_ascii_case("over")
26553            {
26554                self.advance();
26555                // v7.39 (round 230) — PG implements neither modifier for a
26556                // windowed call and says so (0A000). Both used to be parsed
26557                // and then silently dropped here, so `count(DISTINCT v)
26558                // OVER (…)` quietly answered the non-distinct count.
26559                if agg_distinct {
26560                    return Err(
26561                        self.err("DISTINCT is not implemented for window functions".to_string())
26562                    );
26563                }
26564                if !agg_order_by.is_empty() {
26565                    // PG separates the two shapes that land here: a
26566                    // WITHIN GROUP call is an ordered-set aggregate and gets
26567                    // its own message naming the aggregate; a plain
26568                    // `agg(x ORDER BY y)` gets the generic one.
26569                    let msg = if within_group_seen {
26570                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26571                    } else {
26572                        "aggregate ORDER BY is not implemented for window functions".to_string()
26573                    };
26574                    return Err(self.err(msg));
26575                }
26576                let (partition_by, order_by, frame) = self.parse_over_clause()?;
26577                return Ok(Expr::WindowFunction {
26578                    name: first,
26579                    args,
26580                    partition_by,
26581                    order_by,
26582                    frame,
26583                    null_treatment,
26584                    filter,
26585                });
26586            }
26587            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26588                return Ok(Expr::AggregateOrdered {
26589                    call: Box::new(Expr::FunctionCall { name: first, args }),
26590                    order_by: agg_order_by,
26591                    distinct: agg_distinct,
26592                    filter,
26593                });
26594            }
26595            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26596            // over TIMESTAMPTZ and has no timestamp overload, so a
26597            // timestamp argument is coerced on the way in and the answer
26598            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26599            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26600            // zone`. SPG answered `timestamp without time zone`, dropping
26601            // the offset from every rendering.
26602            //
26603            // Writing the coercion PG performs makes the existing
26604            // argument-driven typing (the one `date_trunc` uses) reach the
26605            // right answer, rather than teaching the type layer a second
26606            // rule. MySQL's DATE_ADD is a different function that returns
26607            // DATE or DATETIME, so this is PG-dialect only.
26608            //
26609            // Out-of-line because this sits on the RECURSIVE descent
26610            // frame: an inline block with locals here costs every nesting
26611            // level, and the suite's deep-nesting sentinel overflowed the
26612            // 512 KiB parser stack the moment one was added (round 430's
26613            // lesson, in the same shape).
26614            if !self.mysql_dialect {
26615                lift_date_add_arg_to_timestamptz(&first, &mut args);
26616            }
26617            return Ok(Expr::FunctionCall { name: first, args });
26618        }
26619        // v7.9.20 — SQL-standard parenless keyword expressions
26620        // (PG treats these as functions called without parens).
26621        // Resolve to a synthetic FunctionCall so the engine's
26622        // eval path reuses the existing function-call routing.
26623        // mailrs G3.
26624        let lc = first.to_ascii_lowercase();
26625        if matches!(
26626            lc.as_str(),
26627            "current_date"
26628                | "current_time"
26629                | "current_timestamp"
26630                | "localtimestamp"
26631                | "localtime"
26632                // v7.37.17 (17.6 siblings) — session-identity SQL-
26633                // standard parenless keywords. current_user /
26634                // session_user / user were already caught by the
26635                // pgwire canned-response shortcut but bare-select
26636                // in the embedded engine went through Expr::Column
26637                // and errored. Adding them here so the parser
26638                // resolves to a synthetic FunctionCall that reuses
26639                // the existing eval/functions.rs dispatch.
26640                | "current_user"
26641                | "session_user"
26642                | "current_role"
26643                | "current_catalog"
26644                | "current_schema"
26645                | "current_database"
26646                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
26647                | "system_user"
26648        ) {
26649            return Ok(Expr::FunctionCall {
26650                name: lc,
26651                args: Vec::new(),
26652            });
26653        }
26654        Ok(Expr::Column(ColumnName {
26655            qualifier: None,
26656            name: first,
26657        }))
26658    }
26659}
26660
26661/// v7.39 (round 522) — write the coercion PG's `date_add` /
26662/// `date_subtract` signature performs.
26663///
26664/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
26665/// timestamp argument is cast on the way in and the answer is
26666/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
26667/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
26668/// `timestamp without time zone`, dropping the offset from every
26669/// rendering of the result.
26670///
26671/// Writing the cast the signature implies lets the existing
26672/// argument-driven typing (the one `date_trunc` uses) reach the right
26673/// answer instead of teaching the type layer a second rule. MySQL's
26674/// DATE_ADD is a different function returning DATE or DATETIME, so the
26675/// caller applies this in PG dialect only.
26676///
26677/// A free function, and not a block at the call site, because the caller
26678/// is on the recursive-descent frame chain.
26679#[inline(never)]
26680fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
26681    if args.len() != 2
26682        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
26683    {
26684        return;
26685    }
26686    let base = args.remove(0);
26687    args.insert(
26688        0,
26689        Expr::Cast {
26690            expr: Box::new(base),
26691            target: CastTarget::Timestamptz,
26692        },
26693    );
26694}
26695
26696/// v6.8.2 — walk an expression tree and return the first column
26697/// reference's bare name. Used by `parse_create_index_stmt_after_create`
26698/// to derive `CreateIndexStatement.column` from an expression
26699/// key (so downstream planner code resolving a primary column
26700/// position keeps working with expression indexes). Returns
26701/// `None` when the expression has no column ref at all — caller
26702/// surfaces that as a parse error.
26703fn extract_first_column(expr: &Expr) -> Option<String> {
26704    match expr {
26705        Expr::Column(cn) => Some(cn.name.clone()),
26706        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
26707        Expr::Binary { lhs, rhs, .. } => {
26708            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
26709        }
26710        Expr::Unary { expr: e, .. } => extract_first_column(e),
26711        // v7.39 (read01 round 93) — a cast wraps its operand: a common
26712        // expression-index key is `lower(col::text)`, where the column
26713        // sits under the `::text` cast inside the function arg. Without
26714        // descending here the key was rejected as "references no column".
26715        Expr::Cast { expr: e, .. } => extract_first_column(e),
26716        // v7.39.2 — and a COLLATE wraps its operand the same way.
26717        // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
26718        // column the moment the clause became a node instead of being
26719        // absorbed, and the key was rejected as referencing none. This
26720        // is the shape the wildcard below silently produces, which is
26721        // why it is spelled out.
26722        Expr::Collate { expr: e, .. } => extract_first_column(e),
26723        _ => None,
26724    }
26725}
26726
26727fn maybe_not(expr: Expr, negated: bool) -> Expr {
26728    if negated {
26729        Expr::Unary {
26730            op: UnOp::Not,
26731            expr: Box::new(expr),
26732        }
26733    } else {
26734        expr
26735    }
26736}
26737
26738/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
26739/// things in the two dialects, and SPG read all three PG's way:
26740///
26741/// | token | PG (and SPG) | MySQL, measured |
26742/// |---|---|---|
26743/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
26744/// | `&&` | inet / array overlap | **AND** |
26745/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
26746///
26747/// `1 || 0` answering the string '10' on a MySQL session is a wrong
26748/// answer with no error, which is why they are routed here rather than
26749/// left to the shared table.
26750impl Parser {
26751    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
26752        if self.mysql_dialect {
26753            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
26754            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
26755            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
26756            if let Token::Ident(w) = tok
26757                && w.eq_ignore_ascii_case("div")
26758            {
26759                return Some((BinOp::IntDiv, 8));
26760            }
26761            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
26762            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
26763            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
26764            // there sits in operand position, not infix).
26765            if let Token::Ident(w) = tok
26766                && w.eq_ignore_ascii_case("mod")
26767            {
26768                return Some((BinOp::Mod, 8));
26769            }
26770            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
26771            // plain ident to the lexer. Its precedence sits between OR (1)
26772            // and AND (3) — hence rung 2, the slot freed by moving AND up.
26773            if let Token::Ident(w) = tok
26774                && w.eq_ignore_ascii_case("xor")
26775            {
26776                return Some((BinOp::LogicalXor, 2));
26777            }
26778            match tok {
26779                Token::Concat => return Some((BinOp::Or, 1)),
26780                // MySQL's `&&` is logical AND, sharing AND's rung (3).
26781                Token::InetOverlap => return Some((BinOp::And, 3)),
26782                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
26783                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
26784                _ => {}
26785            }
26786        }
26787        binop_from(tok)
26788    }
26789}
26790
26791// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
26792// (which sits strictly between OR and AND), every level from AND upward was
26793// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
26794// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
26795// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
26796// the *relative* order of every PG operator is unchanged by the shift.
26797fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
26798    let pair = match tok {
26799        Token::Or => (BinOp::Or, 1),
26800        Token::And => (BinOp::And, 3),
26801        Token::Eq => (BinOp::Eq, 5),
26802        Token::NotEq => (BinOp::NotEq, 5),
26803        Token::Lt => (BinOp::Lt, 5),
26804        Token::LtEq => (BinOp::LtEq, 5),
26805        Token::Gt => (BinOp::Gt, 5),
26806        Token::GtEq => (BinOp::GtEq, 5),
26807        // pgvector distance ops all sit on the same rung — tighter than
26808        // comparisons (5) so `col <-> v < threshold` parses correctly.
26809        Token::L2Distance => (BinOp::L2Distance, 6),
26810        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
26811        // comparison rung.
26812        Token::GeomParallel => (BinOp::GeomParallel, 5),
26813        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
26814        // comparison rung.
26815        Token::OverLeft => (BinOp::OverLeft, 5),
26816        Token::OverRight => (BinOp::OverRight, 5),
26817        Token::GeomPerp => (BinOp::GeomPerp, 5),
26818        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
26819        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
26820        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
26821        Token::InnerProduct => (BinOp::InnerProduct, 6),
26822        Token::CosineDistance => (BinOp::CosineDistance, 6),
26823        Token::Plus => (BinOp::Add, 7),
26824        Token::Minus => (BinOp::Sub, 7),
26825        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
26826        // binds every "other" operator (`||`, `|`, `&`, `#`, the
26827        // pgvector distances above) BETWEEN additive (7) and the
26828        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
26829        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
26830        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
26831        // ("matches PG conceptually" — the round-753 audit measured it
26832        // false; the old rung errored on `'a' || 1 + 1` with
26833        // `text + integer`). Same-level chains left-fold, as PG does.
26834        Token::Concat => (BinOp::Concat, 6),
26835        Token::Pipe => (BinOp::BitOr, 6),
26836        Token::Amp => (BinOp::BitAnd, 6),
26837        Token::Star => (BinOp::Mul, 8),
26838        Token::Slash => (BinOp::Div, 8),
26839        Token::Percent => (BinOp::Mod, 8),
26840        // v4.14: JSON path ops bind tighter than comparisons (5)
26841        // and additive (7) so `doc->'k' = 'v'` parses correctly.
26842        // Same rung as the multiplicative ops.
26843        Token::JsonGet => (BinOp::JsonGet, 8),
26844        Token::JsonGetText => (BinOp::JsonGetText, 8),
26845        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
26846        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
26847        Token::JsonContains => (BinOp::JsonContains, 8),
26848        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
26849        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
26850        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
26851        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
26852        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
26853        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
26854        // v7.12.2 — `@@` binds at the comparison rung (looser than
26855        // arithmetic, tighter than AND / OR). PG places `@@` at
26856        // the same precedence as `=` / `<`, so we follow.
26857        Token::TsMatch => (BinOp::TsMatch, 5),
26858        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
26859        // PG places these at the comparison rung (same level as `=`),
26860        // so we follow.
26861        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
26862        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
26863        Token::InetContains => (BinOp::InetContains, 5),
26864        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
26865        Token::InetOverlap => (BinOp::InetOverlap, 5),
26866        // v7.39 (round 508) — the geometric and pattern-order predicates
26867        // ride the comparison rung, as every other predicate does.
26868        Token::Intersects => (BinOp::Intersects, 5),
26869        Token::IsBelow => (BinOp::IsBelow, 5),
26870        Token::IsAbove => (BinOp::IsAbove, 5),
26871        Token::PatternLt => (BinOp::PatternLt, 5),
26872        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
26873        Token::PatternGt => (BinOp::PatternGt, 5),
26874        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
26875        // `@@@` is the old spelling of `@@` and means exactly it.
26876        Token::TsMatchOld => (BinOp::TsMatch, 5),
26877        _ => return None,
26878    };
26879    Some(pair)
26880}
26881
26882#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
26883// `as f32` here is intentional: vector elements widen / narrow into f32 on
26884// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
26885// past ~15 decimal digits — both are acceptable for a fixed-precision
26886// pgvector column.
26887/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
26888/// implicit table alias and break trailing clauses. WITH lands
26889/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
26890/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
26891/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
26892/// / VALUES / FOR / LATERAL — all of which would otherwise be
26893/// silently swallowed by `parse_optional_alias`.
26894fn is_alias_stopword(s: &str) -> bool {
26895    matches!(
26896        s.to_ascii_lowercase().as_str(),
26897        "with"
26898            | "on"
26899            | "where"
26900            | "having"
26901            | "group"
26902            | "order"
26903            | "limit"
26904            | "offset"
26905            | "union"
26906            | "except"
26907            | "intersect"
26908            | "returning"
26909            | "set"
26910            | "values"
26911            | "for"
26912            | "window"
26913            | "tablesample"
26914            | "lateral"
26915            | "left"
26916            | "right"
26917            | "inner"
26918            | "outer"
26919            | "full"
26920            | "cross"
26921            | "join"
26922            | "natural"
26923            | "using"
26924            | "fetch"
26925    )
26926}
26927
26928fn extract_numeric_literal(e: &Expr) -> Option<f32> {
26929    match e {
26930        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
26931        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
26932        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
26933        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
26934        // so scale the divisor by hand instead of `f32::powi`.)
26935        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
26936            let mut div = 1.0f32;
26937            for _ in 0..*scale {
26938                div *= 10.0;
26939            }
26940            Some(*unscaled as f32 / div)
26941        }
26942        Expr::Unary {
26943            op: UnOp::Neg,
26944            expr,
26945        } => extract_numeric_literal(expr).map(|x| -x),
26946        _ => None,
26947    }
26948}
26949
26950/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
26951/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
26952/// negative. Returns `None` if any pair fails to parse or no pair is found.
26953///
26954/// Recognised units (case-insensitive, optional trailing `s`):
26955/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
26956/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
26957/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
26958/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
26959/// (PG-canonical: DST and month-boundary semantics depend on this).
26960/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
26961/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
26962/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
26963#[allow(clippy::cast_possible_truncation)]
26964fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
26965    let mut months: i64 = 0;
26966    let mut days: i64 = 0;
26967    let mut micros: i64 = 0;
26968    let mut in_time = false;
26969    let mut num = alloc::string::String::new();
26970    for ch in rest.chars() {
26971        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
26972            num.push(ch);
26973            continue;
26974        }
26975        if ch == 'T' || ch == 't' {
26976            if !num.is_empty() {
26977                return None;
26978            }
26979            in_time = true;
26980            continue;
26981        }
26982        let n: f64 = num.parse().ok()?;
26983        num.clear();
26984        match (ch, in_time) {
26985            ('Y' | 'y', false) => months += (n * 12.0) as i64,
26986            ('M', false) => months += n as i64,
26987            ('W' | 'w', false) => days += (n * 7.0) as i64,
26988            ('D' | 'd', false) => days += n as i64,
26989            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
26990            ('M', true) => micros += (n * 60_000_000.0) as i64,
26991            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
26992            _ => return None,
26993        }
26994    }
26995    if !num.is_empty() {
26996        return None;
26997    }
26998    Some((
26999        i32::try_from(months).ok()?,
27000        i32::try_from(days).ok()?,
27001        micros,
27002    ))
27003}
27004
27005/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
27006/// leading `-` negates the whole value). Rejects date-like strings.
27007fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
27008    let (neg, body) = match s.strip_prefix('-') {
27009        Some(b) => (true, b),
27010        None => (false, s),
27011    };
27012    let (y, m) = body.split_once('-')?;
27013    let years: i32 = y.parse().ok()?;
27014    let mons: i32 = m.parse().ok()?;
27015    if years < 0 || mons < 0 {
27016        return None;
27017    }
27018    let total = years.checked_mul(12)?.checked_add(mons)?;
27019    Some((if neg { -total } else { total }, 0, 0))
27020}
27021
27022/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
27023/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
27024fn parse_interval_clock(tok: &str) -> Option<i64> {
27025    let (neg, body) = match tok.strip_prefix('-') {
27026        Some(r) => (true, r),
27027        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
27028    };
27029    let mut it = body.split(':');
27030    let h: i64 = it.next()?.parse().ok()?;
27031    let m: i64 = it.next()?.parse().ok()?;
27032    let s_tok = it.next().unwrap_or("0");
27033    if it.next().is_some() {
27034        return None;
27035    }
27036    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27037        let sec: i64 = sec.parse().ok()?;
27038        let mut f = alloc::string::String::from(frac);
27039        while f.len() < 6 {
27040            f.push('0');
27041        }
27042        f.truncate(6);
27043        let fus: i64 = f.parse().ok()?;
27044        sec.checked_mul(1_000_000)?.checked_add(fus)?
27045    } else {
27046        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27047    };
27048    let total = h
27049        .checked_mul(3_600_000_000)?
27050        .checked_add(m.checked_mul(60_000_000)?)?
27051        .checked_add(sec_us)?;
27052    Some(if neg { -total } else { total })
27053}
27054
27055/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27056/// every spelling PG accepts (measured against live PG18.4, not guessed):
27057/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27058/// Before this, the unit table matched long names only, with an ad-hoc
27059/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27060/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27061/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27062/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27063/// fractional) both read from this one table now.
27064fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27065    let u = raw.to_ascii_lowercase();
27066    Some(match u.as_str() {
27067        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27068            "microsecond"
27069        }
27070        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27071            "millisecond"
27072        }
27073        "second" | "seconds" | "sec" | "secs" | "s" => "second",
27074        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27075        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27076        "day" | "days" | "d" => "day",
27077        "week" | "weeks" | "w" => "week",
27078        "month" | "months" | "mon" | "mons" => "month",
27079        "year" | "years" | "yr" | "yrs" | "y" => "year",
27080        "decade" | "decades" | "dec" | "decs" => "decade",
27081        "century" | "centuries" | "cent" | "c" => "century",
27082        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27083        _ => return None,
27084    })
27085}
27086
27087/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27088/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27089#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27090pub(crate) enum IntervalField {
27091    Year,
27092    Month,
27093    Day,
27094    Hour,
27095    Minute,
27096    Second,
27097}
27098
27099/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27100/// spellings aren't standard for the qualifier position, so only the singular
27101/// forms are accepted.
27102/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27103/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27104/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27105/// take a `'1 2'` style literal — are not read here; they stay a parse
27106/// error rather than being silently misread.)
27107/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27108///
27109/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27110/// to do with a `@@` engine setting, and an unset one reads NULL rather
27111/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27112/// were the same node and `SELECT @x` answered "Unknown system variable".)
27113/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27114/// not see a session override — measured, after `SET autocommit=0`,
27115/// `@@global.autocommit` is still 1.
27116///
27117/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27118/// the parser's nesting budget is tuned against, and building these
27119/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27120/// wall `parse_left_right_atom` and friends were factored out for).
27121#[inline(never)]
27122fn variable_ref_atom(raw: &str) -> Expr {
27123    let user_var = !raw.starts_with("@@");
27124    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27125    Expr::FunctionCall {
27126        name: String::from(if user_var {
27127            "__spg_user_var"
27128        } else {
27129            "__spg_session_var"
27130        }),
27131        args: alloc::vec![Expr::Literal(Literal::String(bare))],
27132    }
27133}
27134
27135fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27136    let Token::Ident(s) = tok else { return None };
27137    Some(match () {
27138        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27139        () if s.eq_ignore_ascii_case("second") => "second",
27140        () if s.eq_ignore_ascii_case("minute") => "minute",
27141        () if s.eq_ignore_ascii_case("hour") => "hour",
27142        () if s.eq_ignore_ascii_case("day") => "day",
27143        () if s.eq_ignore_ascii_case("week") => "week",
27144        () if s.eq_ignore_ascii_case("month") => "month",
27145        () if s.eq_ignore_ascii_case("quarter") => "quarter",
27146        () if s.eq_ignore_ascii_case("year") => "year",
27147        () => return None,
27148    })
27149}
27150
27151/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27152/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27153/// which constructs the value at run time. Only the slot the unit names
27154/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27155/// slot the builtin has (months and fractional seconds respectively).
27156fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27157    let zero = || Expr::Literal(Literal::Integer(0));
27158    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27159        lhs: alloc::boxed::Box::new(qty.clone()),
27160        op,
27161        rhs: alloc::boxed::Box::new(by),
27162    };
27163    // (years, months, weeks, days, hours, mins, secs)
27164    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27165    match unit {
27166        "year" => args[0] = qty,
27167        "quarter" => {
27168            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27169        }
27170        "month" => args[1] = qty,
27171        "week" => args[2] = qty,
27172        "day" => args[3] = qty,
27173        "hour" => args[4] = qty,
27174        "minute" => args[5] = qty,
27175        "second" => args[6] = qty,
27176        // The builtin's seconds slot takes a fraction, so microseconds ride
27177        // it scaled down; the divisor is a NUMERIC literal so the division
27178        // stays exact rather than going through a float.
27179        "microsecond" => {
27180            args[6] = scaled(
27181                crate::ast::BinOp::Div,
27182                Expr::Literal(Literal::Numeric {
27183                    unscaled: 1_000_000,
27184                    scale: 0,
27185                }),
27186            );
27187        }
27188        _ => args[3] = qty,
27189    }
27190    Expr::FunctionCall {
27191        name: alloc::string::String::from("make_interval"),
27192        args,
27193    }
27194}
27195
27196/// `(count, unit)` → `(months, days, micros)`.
27197fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27198    let n: i64 = count.trim().parse().ok()?;
27199    Some(match unit {
27200        "microsecond" => (0, 0, n),
27201        "second" => (0, 0, n.checked_mul(1_000_000)?),
27202        "minute" => (0, 0, n.checked_mul(60_000_000)?),
27203        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27204        "day" => (0, i32::try_from(n).ok()?, 0),
27205        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27206        "month" => (i32::try_from(n).ok()?, 0, 0),
27207        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27208        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27209        _ => return None,
27210    })
27211}
27212
27213fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27214    let Token::Ident(s) = tok else { return None };
27215    Some(match () {
27216        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27217        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27218        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27219        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27220        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27221        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27222        () => return None,
27223    })
27224}
27225
27226/// v7.39 (read01 round 102) — interpret an interval literal under a field
27227/// qualifier. Returns `(months, days, micros)`.
27228///
27229/// * A single field applied to a bare number sets which unit the number means,
27230///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27231///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27232/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27233/// * Every other range, and any literal a single field can't read as a plain
27234///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27235///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27236///   like PG, and the qualifier there only bounds precision.
27237fn interpret_qualified_interval(
27238    text: &str,
27239    (f1, f2): (IntervalField, Option<IntervalField>),
27240) -> Option<(i32, i32, i64)> {
27241    if let Some(f2) = f2 {
27242        if f1 == IntervalField::Year && f2 == IntervalField::Month {
27243            if let Some(m) = parse_year_month_literal(text) {
27244                return Some((m, 0, 0));
27245            }
27246        }
27247        return parse_interval_text(text);
27248    }
27249    // Single field: reinterpret a bare number; otherwise the default parse.
27250    let trimmed = text.trim();
27251    if let Ok(val) = trimmed.parse::<f64>() {
27252        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27253        #[allow(clippy::cast_possible_truncation)]
27254        let whole = val as i64;
27255        #[allow(clippy::cast_possible_truncation)]
27256        let secs_micros = {
27257            let m = val * 1_000_000.0;
27258            if m >= 0.0 {
27259                (m + 0.5) as i64
27260            } else {
27261                (m - 0.5) as i64
27262            }
27263        };
27264        return Some(match f1 {
27265            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27266            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27267            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27268            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27269            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27270            IntervalField::Second => (0, 0, secs_micros),
27271        });
27272    }
27273    parse_interval_text(text)
27274}
27275
27276/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27277fn parse_year_month_literal(text: &str) -> Option<i32> {
27278    let t = text.trim();
27279    let (neg, body) = match t.strip_prefix('-') {
27280        Some(r) => (true, r),
27281        None => (false, t.strip_prefix('+').unwrap_or(t)),
27282    };
27283    let mut it = body.split('-');
27284    let years: i32 = it.next()?.trim().parse().ok()?;
27285    let months: i32 = match it.next() {
27286        Some(m) => m.trim().parse().ok()?,
27287        None => 0,
27288    };
27289    if it.next().is_some() {
27290        return None;
27291    }
27292    let total = years.checked_mul(12)?.checked_add(months)?;
27293    Some(if neg { -total } else { total })
27294}
27295
27296pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27297    // v7.38.19 — the two infinities, answered as the three extreme
27298    // fields PostgreSQL itself puts on the wire for them:
27299    //
27300    //   COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27301    //     … 7fffffffffffffff 7fffffff 7fffffff
27302    //
27303    // So no caller has to know the spelling — every one of them already
27304    // reads the three numbers, and `IntervalKind::from_fields` names
27305    // what they mean.
27306    //
27307    // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27308    // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27309    // infinity. Interval takes the full word, in any case.
27310    {
27311        let word = s.trim();
27312        let word = word.strip_prefix('@').map_or(word, str::trim);
27313        let (neg, body) = match word.strip_prefix('-') {
27314            Some(rest) => (true, rest.trim_start()),
27315            None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27316        };
27317        if body.eq_ignore_ascii_case("infinity") {
27318            return Some(if neg {
27319                (i32::MIN, i32::MIN, i64::MIN)
27320            } else {
27321                (i32::MAX, i32::MAX, i64::MAX)
27322            });
27323        }
27324    }
27325    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27326    // `@` is decorative; a trailing `ago` negates the whole interval.
27327    let mut trimmed = s.trim();
27328    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27329    let mut negate = false;
27330    if let Some(rest) = trimmed
27331        .strip_suffix("ago")
27332        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27333    {
27334        negate = true;
27335        trimmed = rest.trim();
27336    }
27337    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27338        let (mo, d, us) = v?;
27339        if negate {
27340            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27341        } else {
27342            Some((mo, d, us))
27343        }
27344    };
27345    let s = trimmed;
27346    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27347    // are single tokens, not the `<n> <unit>` pair form handled below.
27348    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27349        return finish(parse_iso8601_interval(rest));
27350    }
27351    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27352        if let Some(iv) = parse_year_month_interval(trimmed) {
27353            return finish(Some(iv));
27354        }
27355    }
27356    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27357    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27358    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27359    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27360        if let Ok(n) = trimmed.parse::<i64>() {
27361            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27362        }
27363        if let Ok(f) = trimmed.parse::<f64>() {
27364            if f.is_finite() {
27365                #[allow(clippy::cast_possible_truncation)]
27366                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27367            }
27368        }
27369    }
27370    // v7.39 (round 243) — PG accepts the number and unit run together
27371    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27372    // the `<n> <unit>` pair loop below sees them as two.
27373    let raw_parts: Vec<&str> = s.split_whitespace().collect();
27374    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27375    for p in raw_parts {
27376        let boundary = p
27377            .char_indices()
27378            .find(|(i, c)| {
27379                *i > 0
27380                    && c.is_ascii_alphabetic()
27381                    && p[..*i]
27382                        .chars()
27383                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27384                    && p[..*i].chars().any(|d| d.is_ascii_digit())
27385            })
27386            .map(|(i, _)| i);
27387        match boundary {
27388            Some(i) => {
27389                parts.push(&p[..i]);
27390                parts.push(&p[i..]);
27391            }
27392            None => parts.push(p),
27393        }
27394    }
27395    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27396    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27397    // remains is the `<n> <unit>` pair form handled below.
27398    let mut clock_us: i64 = 0;
27399    let mut had_clock = false;
27400    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27401        clock_us = parse_interval_clock(parts[pos])?;
27402        parts.remove(pos);
27403        had_clock = true;
27404    }
27405    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27406    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27407    let mut lone_days: i32 = 0;
27408    if had_clock && parts.len() == 1 {
27409        if let Ok(n) = parts[0].parse::<i64>() {
27410            lone_days = i32::try_from(n).ok()?;
27411            parts.clear();
27412        }
27413    }
27414    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27415        return None;
27416    }
27417    let mut months: i32 = 0;
27418    let mut days: i32 = lone_days;
27419    let mut micros: i64 = clock_us;
27420    let mut i = 0;
27421    while i < parts.len() {
27422        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27423        if let Ok(n) = parts[i].parse::<i64>() {
27424            match unit_stripped {
27425                "microsecond" => micros = micros.checked_add(n)?,
27426                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27427                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27428                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27429                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27430                "day" => {
27431                    let n32 = i32::try_from(n).ok()?;
27432                    days = days.checked_add(n32)?;
27433                }
27434                "week" => {
27435                    let n32 = i32::try_from(n).ok()?;
27436                    days = days.checked_add(n32.checked_mul(7)?)?;
27437                }
27438                "month" => {
27439                    let n32 = i32::try_from(n).ok()?;
27440                    months = months.checked_add(n32)?;
27441                }
27442                "year" => {
27443                    let n32 = i32::try_from(n).ok()?;
27444                    months = months.checked_add(n32.checked_mul(12)?)?;
27445                }
27446                // v7.39 (read01 timestamp.c) — the larger calendar units.
27447                "decade" => {
27448                    let n32 = i32::try_from(n).ok()?;
27449                    months = months.checked_add(n32.checked_mul(120)?)?;
27450                }
27451                "century" => {
27452                    let n32 = i32::try_from(n).ok()?;
27453                    months = months.checked_add(n32.checked_mul(1200)?)?;
27454                }
27455                "millennium" => {
27456                    let n32 = i32::try_from(n).ok()?;
27457                    months = months.checked_add(n32.checked_mul(12000)?)?;
27458                }
27459                _ => return None,
27460            }
27461        } else if let Ok(f) = parts[i].parse::<f64>() {
27462            // Fractional units cascade down to the next-finer field the way
27463            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27464            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27465            // no_std: f64 has no trunc/fract/round methods, so do them with
27466            // casts (toward-zero) + explicit round-half-away-from-zero.
27467            #[allow(clippy::cast_possible_truncation)]
27468            fn round_i64(x: f64) -> i64 {
27469                if x >= 0.0 {
27470                    (x + 0.5) as i64
27471                } else {
27472                    (x - 0.5) as i64
27473                }
27474            }
27475            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27476            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27477                const DAY_US: f64 = 86_400_000_000.0;
27478                let whole = d as i64; // truncates toward zero
27479                let frac = d - whole as f64;
27480                *days = days.checked_add(i32::try_from(whole).ok()?)?;
27481                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27482                Some(())
27483            }
27484            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27485            match unit_stripped {
27486                "microsecond" => micros = micros.checked_add(round_i64(f))?,
27487                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27488                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27489                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27490                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27491                "day" => add_days_frac(&mut days, &mut micros, f)?,
27492                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27493                "month" => {
27494                    let whole = f as i64;
27495                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27496                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27497                }
27498                "year" => {
27499                    let m = f * 12.0;
27500                    let whole = m as i64;
27501                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27502                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27503                }
27504                _ => return None,
27505            }
27506        } else {
27507            return None;
27508        }
27509        i += 2;
27510    }
27511    finish(Some((months, days, micros)))
27512}
27513
27514/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
27515/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
27516/// `interval` is intentionally absent (handled by its own parser arm).
27517/// Returns `None` for names that aren't sensible as a bare typed literal, so
27518/// the caller falls back to treating the ident as a column reference.
27519fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
27520    Some(match ident {
27521        "date" => CastTarget::Date,
27522        "timestamp" | "datetime" => CastTarget::Timestamp,
27523        "timestamptz" => CastTarget::Timestamptz,
27524        "bool" | "boolean" => CastTarget::Bool,
27525        "int" | "integer" | "int4" => CastTarget::Int,
27526        "bigint" | "int8" => CastTarget::BigInt,
27527        "float8" | "double precision" => CastTarget::Float,
27528        "uuid" => CastTarget::Uuid,
27529        "bytea" => CastTarget::Bytea,
27530        "json" => CastTarget::Json,
27531        "jsonb" => CastTarget::Jsonb,
27532        // Types without a dedicated CastTarget variant flow through the
27533        // generic Named path (engine resolves via column_type_to_data_type).
27534        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
27535        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
27536        | "money" | "bit" | "varbit"
27537        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
27538        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
27539        // Range / multirange types likewise.
27540        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
27541        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
27542        | "datemultirange" | "tsmultirange" | "tstzmultirange"
27543        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
27544        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
27545            CastTarget::Named(alloc::string::String::from(ident))
27546        }
27547        _ => return None,
27548    })
27549}
27550
27551/// v7.12.4 — map a bare type-name identifier (the form that
27552/// appears in a function arg list or RETURNS clause) to a
27553/// [`ColumnTypeName`]. Returns `None` for unknown / extension
27554/// types so the caller can preserve them as
27555/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
27556///
27557/// Subset of the full column-type grammar — we deliberately
27558/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
27559/// here because function-arg types in v7.12.4 are mostly the
27560/// bare form (`text`, `int`, `bytea`, …).
27561/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27562/// than being `name TYPE`?
27563///
27564/// The multi-word spellings SQL allows for a bare argument type, each
27565/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27566///
27567/// NOTE this list also exists in `spg-storage`, which computes the
27568/// signature key from the rendered argument text and has to reach the
27569/// same verdict. The two crates are siblings — neither depends on the
27570/// other — and each already carries its own table of type spellings
27571/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27572/// there), so this follows the structure rather than inventing new
27573/// duplication. Recorded as V49.
27574pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27575    let t = phrase.trim().to_ascii_lowercase();
27576    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27577    matches!(
27578        base,
27579        "double precision"
27580            | "character varying"
27581            | "bit varying"
27582            | "timestamp with time zone"
27583            | "timestamp without time zone"
27584            | "time with time zone"
27585            | "time without time zone"
27586            | "national character"
27587            | "national character varying"
27588    )
27589}
27590
27591fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27592    Some(match ident.to_ascii_lowercase().as_str() {
27593        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27594        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27595        "bigint" => ColumnTypeName::BigInt,
27596        "float" | "double" => ColumnTypeName::Float,
27597        // v7.39 (round 269) — real is 32-bit.
27598        "real" | "float4" => ColumnTypeName::Real,
27599        "text" => ColumnTypeName::Text,
27600        "bool" | "boolean" => ColumnTypeName::Bool,
27601        "date" => ColumnTypeName::Date,
27602        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27603        "timestamptz" => ColumnTypeName::Timestamptz,
27604        "json" => ColumnTypeName::Json,
27605        "jsonb" => ColumnTypeName::Jsonb,
27606        "bytea" | "bytes" => ColumnTypeName::Bytes,
27607        "tsvector" => ColumnTypeName::TsVector,
27608        "tsquery" => ColumnTypeName::TsQuery,
27609        "uuid" => ColumnTypeName::Uuid,
27610        "interval" => ColumnTypeName::Interval,
27611        "time" => ColumnTypeName::Time,
27612        "year" => ColumnTypeName::Year,
27613        "timetz" => ColumnTypeName::TimeTz,
27614        "money" => ColumnTypeName::Money,
27615        _ => return None,
27616    })
27617}
27618
27619/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27620/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27621///
27622/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
27623/// / embedded SQL land in v7.12.5+):
27624///
27625/// ```text
27626///   body          := [ws] block [ws]
27627///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
27628///   stmt          := assign | return
27629///   assign        := assign_target := expr
27630///   assign_target := ( NEW | OLD ) . ident | ident
27631///   return        := RETURN ( NEW | OLD | NULL | expr )
27632/// ```
27633///
27634/// `expr` is parsed by recursing into the regular `Parser` — so a
27635/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
27636/// NEW.subject || ' ' || NEW.sender)` body shape works without
27637/// the body parser knowing what `to_tsvector` is.
27638///
27639/// Errors here cause the caller to fall back to
27640/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
27641/// successful, but the executor will refuse to invoke the
27642/// function with an "unparseable body" error.
27643/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
27644/// from the crate root as `spg_sql::parse_function_body`.
27645pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27646    parse_plpgsql_body(body)
27647}
27648
27649fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
27650    // Use the regular lexer on the body text. The trailing
27651    // `END;` may or may not have a semicolon; the lexer treats
27652    // both forms identically.
27653    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
27654        message: alloc::format!("plpgsql body lex error: {e}"),
27655        token_pos: 0,
27656    })?;
27657    let mut parser = Parser::new(tokens);
27658    parser.parse_plpgsql_block()
27659}
27660
27661/// v7.39 (GUC) — the textual body of a SET value, for list joining.
27662fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
27663    match v {
27664        crate::ast::SetValue::String(s)
27665        | crate::ast::SetValue::Ident(s)
27666        | crate::ast::SetValue::Number(s) => s.clone(),
27667        crate::ast::SetValue::Default => "DEFAULT".into(),
27668    }
27669}
27670
27671/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
27672/// contains an aggregate call at ITS OWN query level (recursion stops at
27673/// sublink boundaries — a sublink's aggregates belong to the sublink).
27674/// Backs the "aggregate functions are not allowed in a recursive query's
27675/// recursive term" well-formedness check.
27676fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
27677    const AGG_NAMES: &[&str] = &[
27678        "count",
27679        "sum",
27680        "min",
27681        "max",
27682        "avg",
27683        "string_agg",
27684        "array_agg",
27685        "bool_and",
27686        "bool_or",
27687        "every",
27688        "any_value",
27689        "json_agg",
27690        "jsonb_agg",
27691        "json_object_agg",
27692        "jsonb_object_agg",
27693        "bit_and",
27694        "bit_or",
27695        "bit_xor",
27696        "var_pop",
27697        "var_samp",
27698        "variance",
27699        "stddev",
27700        "stddev_pop",
27701        "stddev_samp",
27702        "range_agg",
27703        "range_intersect_agg",
27704        "percentile_cont",
27705        "percentile_disc",
27706        "mode",
27707        "corr",
27708        "covar_pop",
27709        "covar_samp",
27710    ];
27711    match e {
27712        Expr::AggregateOrdered { .. } => true,
27713        Expr::FunctionCall { name, args } => {
27714            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
27715                || args.iter().any(expr_has_toplevel_aggregate)
27716        }
27717        Expr::NamedArg { expr, .. }
27718        | Expr::Variadic(expr)
27719        | Expr::Unary { expr, .. }
27720        | Expr::Cast { expr, .. }
27721        | Expr::IsNull { expr, .. }
27722        | Expr::FieldAccess { base: expr, .. }
27723        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
27724        Expr::Binary { lhs, rhs, .. } => {
27725            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
27726        }
27727        Expr::Like { expr, pattern, .. } => {
27728            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
27729        }
27730        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
27731        Expr::InList { expr, list, .. } => {
27732            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
27733        }
27734        Expr::ArraySubscript { target, index } => {
27735            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
27736        }
27737        Expr::ArraySlice { target, lo, hi } => {
27738            expr_has_toplevel_aggregate(target)
27739                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
27740                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
27741        }
27742        Expr::AnyAll { expr, array, .. } => {
27743            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
27744        }
27745        Expr::Case {
27746            operand,
27747            branches,
27748            else_branch,
27749        } => {
27750            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
27751                || branches
27752                    .iter()
27753                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
27754                || else_branch
27755                    .as_deref()
27756                    .is_some_and(expr_has_toplevel_aggregate)
27757        }
27758        // The outer-level operands of a sublink can aggregate; the sublink's
27759        // own body cannot leak its aggregates up here.
27760        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
27761        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
27762            row.iter().any(expr_has_toplevel_aggregate)
27763        }
27764        _ => false,
27765    }
27766}
27767
27768/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
27769/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
27770/// named table anywhere in its subtree. A plain FROM derived table is NOT a
27771/// sublink and is legal in a recursive term, so it is not walked here.
27772fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
27773    let mut exprs: Vec<&Expr> = Vec::new();
27774    for it in &s.items {
27775        if let crate::ast::SelectItem::Expr { expr, .. } = it {
27776            exprs.push(expr);
27777        }
27778    }
27779    if let Some(w) = &s.where_ {
27780        exprs.push(w);
27781    }
27782    if let Some(h) = &s.having {
27783        exprs.push(h);
27784    }
27785    if let Some(g) = &s.group_by {
27786        exprs.extend(g.iter());
27787    }
27788    if let Some(from) = &s.from {
27789        for j in &from.joins {
27790            if let Some(on) = &j.on {
27791                exprs.push(on);
27792            }
27793        }
27794    }
27795    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
27796}
27797
27798/// Does this expression contain a sublink whose subquery mentions `name`?
27799fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
27800    match e {
27801        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
27802        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
27803        Expr::InSubquery { expr, subquery, .. } => {
27804            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
27805        }
27806        Expr::RowInSubquery { row, subquery, .. } => {
27807            row.iter().any(|x| expr_sublink_mentions(x, name))
27808                || select_mentions_table(subquery, name)
27809        }
27810        Expr::RowCmpSubquery { row, subquery, .. } => {
27811            row.iter().any(|x| expr_sublink_mentions(x, name))
27812                || select_mentions_table(subquery, name)
27813        }
27814        Expr::NamedArg { expr, .. }
27815        | Expr::Variadic(expr)
27816        | Expr::Unary { expr, .. }
27817        | Expr::Cast { expr, .. }
27818        | Expr::IsNull { expr, .. }
27819        | Expr::FieldAccess { base: expr, .. }
27820        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
27821        Expr::Binary { lhs, rhs, .. } => {
27822            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
27823        }
27824        Expr::Like { expr, pattern, .. } => {
27825            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
27826        }
27827        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
27828            args.iter().any(|x| expr_sublink_mentions(x, name))
27829        }
27830        Expr::InList { expr, list, .. } => {
27831            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
27832        }
27833        Expr::ArraySubscript { target, index } => {
27834            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
27835        }
27836        Expr::ArraySlice { target, lo, hi } => {
27837            expr_sublink_mentions(target, name)
27838                || lo
27839                    .as_deref()
27840                    .is_some_and(|x| expr_sublink_mentions(x, name))
27841                || hi
27842                    .as_deref()
27843                    .is_some_and(|x| expr_sublink_mentions(x, name))
27844        }
27845        Expr::AnyAll { expr, array, .. } => {
27846            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
27847        }
27848        Expr::Case {
27849            operand,
27850            branches,
27851            else_branch,
27852        } => {
27853            operand
27854                .as_deref()
27855                .is_some_and(|x| expr_sublink_mentions(x, name))
27856                || branches
27857                    .iter()
27858                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
27859                || else_branch
27860                    .as_deref()
27861                    .is_some_and(|x| expr_sublink_mentions(x, name))
27862        }
27863        _ => false,
27864    }
27865}
27866
27867/// Does this SELECT (in full — FROM tables, derived tables, its own
27868/// sublinks, and union arms) mention the named table?
27869fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
27870    if let Some(from) = &s.from {
27871        if from.primary.name.eq_ignore_ascii_case(name) {
27872            return true;
27873        }
27874        if let Some(sub) = &from.primary.lateral_subquery
27875            && select_mentions_table(sub, name)
27876        {
27877            return true;
27878        }
27879        for j in &from.joins {
27880            if j.table.name.eq_ignore_ascii_case(name) {
27881                return true;
27882            }
27883            if let Some(sub) = &j.table.lateral_subquery
27884                && select_mentions_table(sub, name)
27885            {
27886                return true;
27887            }
27888        }
27889    }
27890    if select_has_self_ref_in_sublink(s, name) {
27891        return true;
27892    }
27893    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
27894}
27895
27896/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
27897/// row count, the way PG evaluates one before applying it.
27898///
27899/// `None` = not a constant (a column, a subquery, a function call).
27900/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
27901/// message stands in for LIMIT / OFFSET, which the caller substitutes.
27902/// All wordings were read off live PG 18.4.
27903fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
27904    use crate::ast::{BinOp, Expr, Literal, UnOp};
27905    match e {
27906        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
27907        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27908            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
27909        }
27910        // PG coerces a string by its CONTENT, and fails on the value.
27911        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
27912            |_| {
27913                Err(alloc::format!(
27914                    "invalid input syntax for type bigint: \"{t}\""
27915                ))
27916            },
27917            |n| Ok(i128::from(n)),
27918        )),
27919        Expr::Literal(Literal::Bool(_)) => Some(Err(
27920            "argument of {L} must be type bigint, not type boolean".into(),
27921        )),
27922        Expr::Unary {
27923            op: UnOp::Neg,
27924            expr,
27925        } => match fold_limit_constant(expr)? {
27926            Ok(v) => Some(Ok(-v)),
27927            e @ Err(_) => Some(e),
27928        },
27929        Expr::Binary { lhs, op, rhs } => {
27930            let a = match fold_limit_constant(lhs)? {
27931                Ok(v) => v,
27932                e @ Err(_) => return Some(e),
27933            };
27934            let b = match fold_limit_constant(rhs)? {
27935                Ok(v) => v,
27936                e @ Err(_) => return Some(e),
27937            };
27938            let out = match op {
27939                BinOp::Add => a.checked_add(b),
27940                BinOp::Sub => a.checked_sub(b),
27941                BinOp::Mul => a.checked_mul(b),
27942                BinOp::Div if b != 0 => a.checked_div(b),
27943                BinOp::Div => return Some(Err("division by zero".into())),
27944                BinOp::Mod if b != 0 => a.checked_rem(b),
27945                BinOp::Mod => return Some(Err("division by zero".into())),
27946                _ => return None,
27947            };
27948            // PG evaluates the arithmetic in the operand's own type, so an
27949            // int-by-int product that leaves int range fails there — before
27950            // the row count is ever looked at.
27951            match out {
27952                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
27953                    Some(Err("integer out of range".into()))
27954                }
27955                Some(v) => Some(Ok(v)),
27956                None => Some(Err("integer out of range".into())),
27957            }
27958        }
27959        _ => None,
27960    }
27961}
27962
27963/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
27964/// cast, which is what makes `LIMIT 2.5` keep three rows.
27965fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
27966    if scale == 0 {
27967        return unscaled;
27968    }
27969    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
27970        return 0;
27971    };
27972    let neg = unscaled < 0;
27973    let mag = unscaled.unsigned_abs() as i128;
27974    let rounded = (mag + div / 2) / div;
27975    if neg { -rounded } else { rounded }
27976}
27977
27978#[cfg(test)]
27979mod tests {
27980    use super::*;
27981    use alloc::string::ToString;
27982
27983    fn parse(s: &str) -> Statement {
27984        parse_statement(s).expect("parse ok")
27985    }
27986
27987    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
27988    // `tables`, `partition`, etc. are unreserved keywords per PG's
27989    // `pg_get_keywords()` and MUST be usable as column / table /
27990    // alias names. Pre-T4 every drop-in user whose schema had one
27991    // of these as a column name (sentori events.release, mailrs
27992    // messages.index in some forks) blew the parser up at CREATE
27993    // TABLE time with "expected identifier, got Release". The
27994    // generalisation lives in `unreserved_keyword_text` + the
27995    // `expect_ident_like` and `parse_atom` arms that consult it.
27996    #[test]
27997    fn release_usable_as_column_name_in_create_table() {
27998        let stmt =
27999            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
28000        if let Statement::CreateTable(t) = stmt {
28001            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
28002            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
28003        } else {
28004            panic!("expected CreateTable");
28005        }
28006    }
28007
28008    #[test]
28009    fn release_usable_as_column_ref_in_select_projection() {
28010        // The sentori `0003_partition_events.sql` INSERT-SELECT
28011        // walk references `release` in both column lists; the
28012        // projection-side use exercises `parse_atom`'s relaxed
28013        // identifier set.
28014        parse("SELECT id, release, payload FROM events WHERE id = 1");
28015    }
28016
28017    #[test]
28018    fn release_usable_as_column_ref_in_insert_column_list() {
28019        // INSERT INTO t (id, release, payload) VALUES (…)
28020        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
28021    }
28022
28023    #[test]
28024    fn alter_column_drop_not_null_uses_keyword_drop_token() {
28025        // Sentori `0013_audit_tombstone.sql` issues
28026        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
28027        // emits Token::Drop (not Ident("drop")); the parser must
28028        // accept both in the ALTER COLUMN sub-dispatch.
28029        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
28030    }
28031
28032    #[test]
28033    fn create_index_accepts_parenthesised_expression_key() {
28034        // sentori `0040_events_bundle_idx.sql` shape — JSONB
28035        // expression index. Pre-T4 the parser bailed at the
28036        // inner `(` with "expected column ident or expression,
28037        // got LParen". The Token::LParen arm in CREATE INDEX
28038        // routes through the expression parser instead.
28039        parse(
28040            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28041             ON events ((payload->'bundle'->>'id'))",
28042        );
28043    }
28044
28045    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28046    // surface as parse errors, never stack overflows (embed hosts
28047    // abort on overflow).
28048    /// The nesting budget is a COUNT; what it has to fit inside is a
28049    /// number of BYTES, and only one of those two is stable across
28050    /// compiler versions. Round 847 measured 30,336 bytes per level
28051    /// after a toolchain move, which puts 64 levels at 1.94 MB and
28052    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28053    /// aborted instead of erroring, which is precisely the outcome it
28054    /// exists to rule out.
28055    ///
28056    /// So the budget is metered rather than assumed. The ceiling leaves
28057    /// the depth SPG advertises fitting in a default 2 MiB thread with
28058    /// room to spare, in the debug build, where frames are widest.
28059    #[test]
28060    fn nesting_frame_cost_stays_under_ceiling() {
28061        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28062        // thread keeps a margin for whatever called the parser.
28063        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28064
28065        frame_meter::reset();
28066        let depth = frame_meter::SAMPLE_HI + 8;
28067        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28068        parse(&sql);
28069
28070        let per_level = frame_meter::bytes_per_level();
28071        {
28072            extern crate std;
28073            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28074        }
28075        assert!(
28076            per_level <= CEILING,
28077            "{per_level} bytes per nesting level exceeds {CEILING}; \
28078             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28079             in parse_expr_inner / parse_unary rather than lowering the \
28080             depth or widening the stack.",
28081            per_level * MAX_NEST_DEPTH
28082        );
28083    }
28084
28085    #[test]
28086    fn nesting_budget_errors_cleanly() {
28087        let depth = MAX_NEST_DEPTH + 50;
28088        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28089        let err = parse_statement(&sql).expect_err("must reject");
28090        assert!(err.message.contains("nests deeper"), "{err:?}");
28091        // Within budget still parses.
28092        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28093        parse(&sql);
28094    }
28095
28096    #[test]
28097    fn binary_chain_budget_errors_cleanly() {
28098        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28099        let err = parse_statement(&sql).expect_err("must reject");
28100        assert!(err.message.contains("chained binary"), "{err:?}");
28101        // Within budget still parses (chain depth ≤ budget is safe
28102        // for recursive eval/drop on 2 MiB stacks).
28103        let sql = format!("SELECT 1{}", " + 1".repeat(200));
28104        parse(&sql);
28105    }
28106
28107    #[test]
28108    fn in_list_unaffected_by_chain_budget() {
28109        // Flat InList: 20k elements parse fine and stay flat.
28110        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28111        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28112        let Statement::Select(s) = parse(&sql) else {
28113            panic!("expected select")
28114        };
28115        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28116            panic!("expected flat InList, got {:?}", s.where_)
28117        };
28118        assert_eq!(list.len(), 20_000);
28119        assert!(!negated);
28120    }
28121
28122    fn lit_int(n: i64) -> Expr {
28123        Expr::Literal(Literal::Integer(n))
28124    }
28125
28126    fn col(name: &str) -> Expr {
28127        Expr::Column(ColumnName {
28128            qualifier: None,
28129            name: name.into(),
28130        })
28131    }
28132
28133    #[test]
28134    fn select_single_integer() {
28135        let s = parse("SELECT 1");
28136        let Statement::Select(s) = s else {
28137            panic!("expected SELECT")
28138        };
28139        assert_eq!(s.items.len(), 1);
28140        assert!(s.from.is_none());
28141        assert!(s.where_.is_none());
28142    }
28143
28144    #[test]
28145    fn select_multiple_literal_kinds() {
28146        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28147        let Statement::Select(s) = s else {
28148            panic!("expected SELECT")
28149        };
28150        assert_eq!(s.items.len(), 5);
28151    }
28152
28153    #[test]
28154    fn select_wildcard_from_table() {
28155        let s = parse("SELECT * FROM users");
28156        let Statement::Select(s) = s else {
28157            panic!("expected SELECT")
28158        };
28159        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28160        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28161    }
28162
28163    #[test]
28164    fn select_with_table_alias() {
28165        let s = parse("SELECT * FROM users AS u");
28166        let Statement::Select(s) = s else {
28167            panic!("expected SELECT")
28168        };
28169        let t = &s.from.as_ref().unwrap().primary;
28170        assert_eq!(t.name, "users");
28171        assert_eq!(t.alias.as_deref(), Some("u"));
28172    }
28173
28174    #[test]
28175    fn select_with_where_eq() {
28176        let s = parse("SELECT a FROM t WHERE a = 1");
28177        let Statement::Select(s) = s else {
28178            panic!("expected SELECT")
28179        };
28180        let w = s.where_.unwrap();
28181        assert_eq!(
28182            w,
28183            Expr::Binary {
28184                lhs: Box::new(col("a")),
28185                op: BinOp::Eq,
28186                rhs: Box::new(lit_int(1)),
28187            }
28188        );
28189    }
28190
28191    #[test]
28192    fn arithmetic_precedence() {
28193        let s = parse("SELECT 1 + 2 * 3");
28194        let Statement::Select(s) = s else {
28195            panic!("expected SELECT")
28196        };
28197        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28198            panic!("wildcard?")
28199        };
28200        assert_eq!(
28201            expr,
28202            &Expr::Binary {
28203                lhs: Box::new(lit_int(1)),
28204                op: BinOp::Add,
28205                rhs: Box::new(Expr::Binary {
28206                    lhs: Box::new(lit_int(2)),
28207                    op: BinOp::Mul,
28208                    rhs: Box::new(lit_int(3)),
28209                }),
28210            }
28211        );
28212    }
28213
28214    #[test]
28215    fn parentheses_override_precedence() {
28216        let s = parse("SELECT (1 + 2) * 3");
28217        let Statement::Select(s) = s else {
28218            panic!("expected SELECT")
28219        };
28220        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28221            panic!()
28222        };
28223        assert_eq!(
28224            expr,
28225            &Expr::Binary {
28226                lhs: Box::new(Expr::Binary {
28227                    lhs: Box::new(lit_int(1)),
28228                    op: BinOp::Add,
28229                    rhs: Box::new(lit_int(2)),
28230                }),
28231                op: BinOp::Mul,
28232                rhs: Box::new(lit_int(3)),
28233            }
28234        );
28235    }
28236
28237    #[test]
28238    fn not_binds_below_comparison() {
28239        // `NOT a = 1` should parse as `NOT (a = 1)`.
28240        let s = parse("SELECT NOT a = 1 FROM t");
28241        let Statement::Select(s) = s else {
28242            panic!("expected SELECT")
28243        };
28244        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28245            panic!()
28246        };
28247        assert_eq!(
28248            expr,
28249            &Expr::Unary {
28250                op: UnOp::Not,
28251                expr: Box::new(Expr::Binary {
28252                    lhs: Box::new(col("a")),
28253                    op: BinOp::Eq,
28254                    rhs: Box::new(lit_int(1)),
28255                }),
28256            }
28257        );
28258    }
28259
28260    #[test]
28261    fn unary_minus_binds_above_multiplication() {
28262        // `-a * 2` should be `(-a) * 2`.
28263        let s = parse("SELECT -a * 2 FROM t");
28264        let Statement::Select(s) = s else {
28265            panic!("expected SELECT")
28266        };
28267        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28268            panic!()
28269        };
28270        assert_eq!(
28271            expr,
28272            &Expr::Binary {
28273                lhs: Box::new(Expr::Unary {
28274                    op: UnOp::Neg,
28275                    expr: Box::new(col("a")),
28276                }),
28277                op: BinOp::Mul,
28278                rhs: Box::new(lit_int(2)),
28279            }
28280        );
28281    }
28282
28283    #[test]
28284    fn qualified_column() {
28285        let s = parse("SELECT t.col FROM t");
28286        let Statement::Select(s) = s else {
28287            panic!("expected SELECT")
28288        };
28289        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28290            panic!()
28291        };
28292        assert_eq!(
28293            expr,
28294            &Expr::Column(ColumnName {
28295                qualifier: Some("t".into()),
28296                name: "col".into()
28297            })
28298        );
28299    }
28300
28301    #[test]
28302    fn select_item_alias_with_as() {
28303        let s = parse("SELECT a AS y FROM t");
28304        let Statement::Select(s) = s else {
28305            panic!("expected SELECT")
28306        };
28307        let SelectItem::Expr { alias, .. } = &s.items[0] else {
28308            panic!()
28309        };
28310        assert_eq!(alias.as_deref(), Some("y"));
28311    }
28312
28313    #[test]
28314    fn trailing_semicolon_accepted() {
28315        let s = parse("SELECT 1;");
28316        let Statement::Select(s) = s else {
28317            panic!("expected SELECT")
28318        };
28319        assert_eq!(s.items.len(), 1);
28320    }
28321
28322    #[test]
28323    fn boolean_chain_with_and_or_not() {
28324        // (NOT a) OR (b AND (NOT c))
28325        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28326        let Statement::Select(s) = s else {
28327            panic!("expected SELECT")
28328        };
28329        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28330            panic!()
28331        };
28332        let expected = Expr::Binary {
28333            lhs: Box::new(Expr::Unary {
28334                op: UnOp::Not,
28335                expr: Box::new(col("a")),
28336            }),
28337            op: BinOp::Or,
28338            rhs: Box::new(Expr::Binary {
28339                lhs: Box::new(col("b")),
28340                op: BinOp::And,
28341                rhs: Box::new(Expr::Unary {
28342                    op: UnOp::Not,
28343                    expr: Box::new(col("c")),
28344                }),
28345            }),
28346        };
28347        assert_eq!(expr, &expected);
28348    }
28349
28350    #[test]
28351    fn empty_input_errors() {
28352        // v7.14.0 — pg_dump preambles emit several comment-only
28353        // / blank-line statements that collapse to Statement::
28354        // Empty rather than a parse error. The old "SELECT in
28355        // message" assertion is stale; verify the new contract:
28356        // empty / whitespace / comment-only input parses to
28357        // Statement::Empty.
28358        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28359        assert!(matches!(
28360            parse_statement("  \n\t ").unwrap(),
28361            Statement::Empty
28362        ));
28363        // Sanity: malformed-but-non-empty still errors.
28364        assert!(parse_statement("SELECT FROM WHERE").is_err());
28365    }
28366
28367    #[test]
28368    fn unmatched_paren_errors() {
28369        assert!(parse_statement("SELECT (1 + 2").is_err());
28370    }
28371
28372    #[test]
28373    fn display_round_trip_simple_select() {
28374        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28375        let text = original.to_string();
28376        let again = parse_statement(&text).expect("re-parse");
28377        assert_eq!(original, again);
28378    }
28379
28380    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28381
28382    #[test]
28383    fn create_table_single_column() {
28384        let s = parse("CREATE TABLE foo (a INT)");
28385        let Statement::CreateTable(c) = s else {
28386            panic!("expected CreateTable")
28387        };
28388        assert_eq!(c.name, "foo");
28389        assert_eq!(c.columns.len(), 1);
28390        assert_eq!(c.columns[0].name, "a");
28391        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28392        assert!(c.columns[0].nullable);
28393    }
28394
28395    #[test]
28396    fn create_table_multi_column_with_not_null_mix() {
28397        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28398        let Statement::CreateTable(c) = s else {
28399            panic!()
28400        };
28401        assert_eq!(c.columns.len(), 4);
28402        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28403        assert!(!c.columns[0].nullable);
28404        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28405        assert!(c.columns[1].nullable);
28406        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28407        assert!(!c.columns[2].nullable);
28408        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28409    }
28410
28411    #[test]
28412    fn create_table_bigint_supported() {
28413        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28414        let Statement::CreateTable(c) = s else {
28415            panic!()
28416        };
28417        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28418    }
28419
28420    #[test]
28421    fn create_table_vector_default_is_f32() {
28422        let s = parse("CREATE TABLE t (v VECTOR(128))");
28423        let Statement::CreateTable(c) = s else {
28424            panic!()
28425        };
28426        assert_eq!(
28427            c.columns[0].ty,
28428            ColumnTypeName::Vector {
28429                dim: 128,
28430                encoding: VecEncoding::F32,
28431            },
28432        );
28433    }
28434
28435    #[test]
28436    fn create_table_vector_using_sq8() {
28437        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28438        // Case-insensitive on both `USING` and the encoding name.
28439        for sql in [
28440            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28441            "CREATE TABLE t (v VECTOR(128) using sq8)",
28442        ] {
28443            let s = parse(sql);
28444            let Statement::CreateTable(c) = s else {
28445                panic!()
28446            };
28447            assert_eq!(
28448                c.columns[0].ty,
28449                ColumnTypeName::Vector {
28450                    dim: 128,
28451                    encoding: VecEncoding::Sq8,
28452                },
28453                "{sql}",
28454            );
28455        }
28456    }
28457
28458    #[test]
28459    fn create_table_vector_using_unknown_errors() {
28460        // v7.16.1 — the inline `USING <encoding>` shape on
28461        // CREATE TABLE column defs was withdrawn before
28462        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28463        // (col vector_<metric>_ops)`; the parser now rejects
28464        // USING at column-list position with a clearer
28465        // "expected ',' or ')'" message. Test asserts the
28466        // current rejection, not the old "unknown vector
28467        // encoding" string.
28468        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28469        assert!(
28470            err.message.contains("USING")
28471                || err.message.contains("using")
28472                || err.message.contains("')'")
28473                || err.message.contains("','"),
28474            "expected USING/column-list rejection, got: {}",
28475            err.message
28476        );
28477    }
28478
28479    #[test]
28480    fn vector_using_sq8_display_roundtrips() {
28481        // The Display impl must produce text that re-parses to the
28482        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28483        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28484        let Statement::CreateTable(c) = s else {
28485            panic!()
28486        };
28487        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28488    }
28489
28490    #[test]
28491    fn parser_recognises_placeholders() {
28492        use crate::ast::{Expr, SelectItem, Statement};
28493        // $N in expression position parses as Expr::Placeholder(N).
28494        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28495        let Statement::Select(sel) = s else { panic!() };
28496        assert!(matches!(
28497            sel.items[0],
28498            SelectItem::Expr {
28499                expr: Expr::Placeholder(1),
28500                alias: None
28501            }
28502        ));
28503        // $2 + 1
28504        let SelectItem::Expr {
28505            expr: Expr::Binary { lhs, rhs, .. },
28506            ..
28507        } = &sel.items[1]
28508        else {
28509            panic!()
28510        };
28511        assert!(matches!(**lhs, Expr::Placeholder(2)));
28512        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
28513        // WHERE x = $3
28514        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
28515            panic!()
28516        };
28517        assert!(matches!(**rhs, Expr::Placeholder(3)));
28518    }
28519
28520    #[test]
28521    fn parser_rejects_dollar_zero() {
28522        // $0 is not valid in PG; the lexer rejects it.
28523        assert!(parse_statement("SELECT $0").is_err());
28524    }
28525
28526    #[test]
28527    fn placeholder_display_roundtrips() {
28528        // The Display impl must produce text that re-lexes to the
28529        // same Placeholder token.
28530        let s = parse("SELECT $42 FROM t");
28531        let printed = s.to_string();
28532        assert!(printed.contains("$42"));
28533        let again = parse(&printed);
28534        assert_eq!(s, again);
28535    }
28536
28537    #[test]
28538    fn alter_index_rebuild_bare() {
28539        use crate::ast::{AlterIndexTarget, Statement};
28540        let s = parse("ALTER INDEX my_idx REBUILD");
28541        let Statement::AlterIndex(a) = s else {
28542            panic!("expected AlterIndex, got {s:?}")
28543        };
28544        assert_eq!(a.name, "my_idx");
28545        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
28546    }
28547
28548    #[test]
28549    fn alter_index_rebuild_with_encoding() {
28550        use crate::ast::{AlterIndexTarget, Statement};
28551        for (sql, want) in [
28552            (
28553                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
28554                VecEncoding::F32,
28555            ),
28556            (
28557                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
28558                VecEncoding::Sq8,
28559            ),
28560            (
28561                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28562                VecEncoding::F16,
28563            ),
28564        ] {
28565            let s = parse(sql);
28566            let Statement::AlterIndex(a) = s else {
28567                panic!("{sql}: expected AlterIndex")
28568            };
28569            assert_eq!(a.name, "my_idx");
28570            assert_eq!(
28571                a.target,
28572                AlterIndexTarget::Rebuild {
28573                    encoding: Some(want)
28574                },
28575                "{sql}"
28576            );
28577        }
28578    }
28579
28580    #[test]
28581    fn alter_index_rebuild_unknown_encoding_errors() {
28582        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28583        assert!(
28584            err.message.contains("unknown vector encoding"),
28585            "got: {}",
28586            err.message
28587        );
28588    }
28589
28590    #[test]
28591    fn alter_index_rebuild_display_roundtrips() {
28592        for (input, want) in [
28593            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28594            (
28595                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28596                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28597            ),
28598            (
28599                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28600                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28601            ),
28602        ] {
28603            let s = parse(input);
28604            assert_eq!(s.to_string(), want);
28605        }
28606    }
28607
28608    #[test]
28609    fn create_table_unknown_type_defers_to_engine() {
28610        // v4.9 picked XML as a parse-time "unsupported column
28611        // type" probe. v7.17.0 Phase 1.4 changed the contract:
28612        // an unknown type ident parses as Text + `user_type_ref`
28613        // so CREATE TABLE can resolve user-defined enum / domain
28614        // types — rejection of truly-unknown types moved to the
28615        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
28616        // to a first-class built-in, so this probe switched to a
28617        // synthetic name nothing in the lexer will ever recognise.
28618        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
28619        let Statement::CreateTable(t) = stmt else {
28620            panic!("expected CreateTable");
28621        };
28622        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
28623    }
28624
28625    #[test]
28626    fn create_table_missing_table_keyword_errors() {
28627        assert!(parse_statement("CREATE x (a INT)").is_err());
28628    }
28629
28630    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
28631    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
28632
28633    #[test]
28634    fn parse_create_table_partition_by_range() {
28635        use crate::ast::{PartitionBySpec, PartitionKindAst};
28636        let stmt = parse_statement(
28637            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
28638             payload JSONB) PARTITION BY RANGE (ts)",
28639        )
28640        .unwrap();
28641        let Statement::CreateTable(t) = stmt else {
28642            panic!("expected CreateTable");
28643        };
28644        assert!(t.partition_of.is_none(), "parent has no partition_of");
28645        assert_eq!(t.columns.len(), 3);
28646        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
28647        assert_eq!(
28648            by,
28649            &PartitionBySpec {
28650                kind: PartitionKindAst::Range,
28651                key_columns: alloc::vec!["ts".to_string()],
28652            }
28653        );
28654        // Display round-trip preserves the suffix. `quote_ident`
28655        // only adds double quotes when the ident needs escaping, so
28656        // a plain `ts` survives bare here.
28657        assert!(
28658            t.to_string().contains("PARTITION BY RANGE (ts)"),
28659            "Display lost PARTITION BY suffix: {t}"
28660        );
28661    }
28662
28663    #[test]
28664    fn parse_create_table_partition_of_range() {
28665        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
28666        let stmt = parse_statement(
28667            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
28668             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
28669        )
28670        .unwrap();
28671        let Statement::CreateTable(t) = stmt else {
28672            panic!("expected CreateTable");
28673        };
28674        assert!(t.columns.is_empty(), "child inherits columns from parent");
28675        assert!(t.partition_by.is_none());
28676        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28677        assert_eq!(of.parent_name, "events_partitioned");
28678        let PartitionOfSpec { bounds, .. } = of.clone();
28679        match bounds {
28680            PartitionOfBoundsAst::Range { lower, upper } => {
28681                assert!(lower.to_string().contains("2026-06-01"));
28682                assert!(upper.to_string().contains("2026-07-01"));
28683            }
28684            other => panic!("expected Range, got {other:?}"),
28685        }
28686        // Display round-trip emits the FOR VALUES tail. `quote_ident`
28687        // skips quotes when not required, so the parent name appears
28688        // bare here.
28689        let s = t.to_string();
28690        assert!(
28691            s.contains("PARTITION OF events_partitioned"),
28692            "Display lost PARTITION OF: {s}"
28693        );
28694        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
28695        assert!(s.contains(") TO ("), "Display lost TO: {s}");
28696    }
28697
28698    #[test]
28699    fn parse_create_table_partition_of_default() {
28700        use crate::ast::PartitionOfBoundsAst;
28701        let stmt =
28702            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
28703                .unwrap();
28704        let Statement::CreateTable(t) = stmt else {
28705            panic!("expected CreateTable");
28706        };
28707        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
28708        assert_eq!(of.parent_name, "events_partitioned");
28709        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
28710        assert!(
28711            t.to_string()
28712                .contains("PARTITION OF events_partitioned DEFAULT"),
28713            "Display lost DEFAULT: {t}"
28714        );
28715    }
28716
28717    #[test]
28718    fn parse_create_table_partition_by_list() {
28719        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
28720        // child with `FOR VALUES IN (lit, lit, …)`.
28721        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28722        let parent =
28723            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
28724                .unwrap();
28725        let Statement::CreateTable(t) = parent else {
28726            panic!("expected CreateTable");
28727        };
28728        let Some(PartitionBySpec {
28729            kind,
28730            ref key_columns,
28731        }) = t.partition_by
28732        else {
28733            panic!("expected PARTITION BY");
28734        };
28735        assert_eq!(kind, PartitionKindAst::List);
28736        assert_eq!(*key_columns, vec!["region".to_string()]);
28737        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
28738
28739        let child = parse_statement(
28740            "CREATE TABLE events_apac PARTITION OF events_listed \
28741             FOR VALUES IN ('jp', 'kr', 'tw')",
28742        )
28743        .unwrap();
28744        let Statement::CreateTable(c) = child else {
28745            panic!("expected CreateTable");
28746        };
28747        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28748        let PartitionOfBoundsAst::List { values } = &of.bounds else {
28749            panic!("expected List bounds, got {:?}", of.bounds);
28750        };
28751        assert_eq!(values.len(), 3);
28752        let disp = c.to_string();
28753        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
28754    }
28755
28756    #[test]
28757    fn parse_create_table_partition_by_hash() {
28758        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
28759        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
28760        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
28761        let parent =
28762            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
28763        let Statement::CreateTable(t) = parent else {
28764            panic!("expected CreateTable");
28765        };
28766        let Some(PartitionBySpec {
28767            kind,
28768            ref key_columns,
28769        }) = t.partition_by
28770        else {
28771            panic!("expected PARTITION BY");
28772        };
28773        assert_eq!(kind, PartitionKindAst::Hash);
28774        assert_eq!(*key_columns, vec!["id".to_string()]);
28775        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
28776
28777        let child = parse_statement(
28778            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
28779             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
28780        )
28781        .unwrap();
28782        let Statement::CreateTable(c) = child else {
28783            panic!("expected CreateTable");
28784        };
28785        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
28786        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
28787            panic!("expected Hash bounds");
28788        };
28789        assert_eq!(modulus, 4);
28790        assert_eq!(remainder, 0);
28791        let disp = c.to_string();
28792        assert!(
28793            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
28794            "Display lost HASH bounds: {disp}"
28795        );
28796
28797        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
28798        let bad = parse_statement(
28799            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
28800             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
28801        );
28802        let msg = format!("{}", bad.unwrap_err());
28803        assert!(
28804            msg.contains("REMAINDER") && msg.contains("MODULUS"),
28805            "expected REMAINDER/MODULUS validation error: {msg}"
28806        );
28807    }
28808
28809    #[test]
28810    fn parse_create_table_partition_of_rejects_columns() {
28811        // v7.37.6-B contract: PARTITION OF children inherit columns
28812        // from the parent; an explicit list MUST surface as a parse
28813        // error rather than getting silently ignored.
28814        let err = parse_statement(
28815            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
28816             FOR VALUES FROM ('a') TO ('b')",
28817        );
28818        assert!(err.is_err(), "expected parse error for explicit columns");
28819        let msg = format!("{}", err.unwrap_err());
28820        assert!(
28821            msg.contains("PARTITION OF") && msg.contains("column"),
28822            "error should mention PARTITION OF + columns: {msg}"
28823        );
28824    }
28825
28826    #[test]
28827    fn insert_single_value() {
28828        let s = parse("INSERT INTO foo VALUES (42)");
28829        let Statement::Insert(i) = s else {
28830            panic!("expected Insert")
28831        };
28832        assert_eq!(i.table, "foo");
28833        assert_eq!(i.rows.len(), 1);
28834        assert_eq!(i.rows[0].len(), 1);
28835        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
28836    }
28837
28838    #[test]
28839    fn insert_multi_value_with_mixed_literals() {
28840        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
28841        let Statement::Insert(i) = s else { panic!() };
28842        assert_eq!(i.rows.len(), 1);
28843        assert_eq!(i.rows[0].len(), 5);
28844    }
28845
28846    #[test]
28847    fn insert_missing_into_errors() {
28848        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
28849    }
28850
28851    #[test]
28852    fn create_table_round_trip() {
28853        let original =
28854            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
28855        let text = original.to_string();
28856        let again = parse_statement(&text).expect("re-parse");
28857        assert_eq!(original, again);
28858    }
28859
28860    #[test]
28861    fn insert_round_trip_with_negation_and_string() {
28862        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
28863        let text = original.to_string();
28864        let again = parse_statement(&text).expect("re-parse");
28865        assert_eq!(original, again);
28866    }
28867
28868    #[test]
28869    fn unknown_keyword_at_statement_start_errors() {
28870        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
28871        // the top-level dispatch still has no branch to take.
28872        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
28873        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
28874    }
28875
28876    // --- v0.8 CREATE INDEX --------------------------------------------------
28877
28878    #[test]
28879    fn create_index_basic() {
28880        let s = parse("CREATE INDEX idx_id ON users (id)");
28881        let Statement::CreateIndex(c) = s else {
28882            panic!("expected CreateIndex")
28883        };
28884        assert_eq!(c.name, "idx_id");
28885        assert_eq!(c.table, "users");
28886        assert_eq!(c.column, "id");
28887    }
28888
28889    #[test]
28890    fn create_index_missing_on_errors() {
28891        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
28892    }
28893
28894    #[test]
28895    fn create_index_missing_paren_errors() {
28896        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
28897    }
28898
28899    #[test]
28900    fn create_index_round_trip() {
28901        let original = parse("CREATE INDEX by_name ON users (name)");
28902        let again = parse_statement(&original.to_string()).unwrap();
28903        assert_eq!(original, again);
28904    }
28905
28906    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
28907
28908    #[test]
28909    fn create_unique_index_basic() {
28910        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
28911        let Statement::CreateIndex(c) = s else {
28912            panic!("expected CreateIndex");
28913        };
28914        assert!(c.is_unique);
28915        assert_eq!(c.column, "a");
28916        assert!(c.partial_predicate.is_none());
28917    }
28918
28919    #[test]
28920    fn create_unique_index_partial() {
28921        // mailrs's email_templates "one default per user" shape.
28922        let s = parse(
28923            "CREATE UNIQUE INDEX idx_email_templates_user_default \
28924             ON email_templates (user_address) WHERE is_default = true",
28925        );
28926        let Statement::CreateIndex(c) = s else {
28927            panic!("expected CreateIndex");
28928        };
28929        assert!(c.is_unique);
28930        assert_eq!(c.table, "email_templates");
28931        assert_eq!(c.column, "user_address");
28932        assert!(c.partial_predicate.is_some());
28933    }
28934
28935    #[test]
28936    fn create_unique_index_composite_with_predicate() {
28937        // mailrs's calendar_events instance: composite columns.
28938        let s = parse(
28939            "CREATE UNIQUE INDEX uq_calendar_events_instance \
28940             ON calendar_events (calendar_id, uid, recurrence_id) \
28941             WHERE recurrence_id IS NOT NULL",
28942        );
28943        let Statement::CreateIndex(c) = s else {
28944            panic!("expected CreateIndex");
28945        };
28946        assert!(c.is_unique);
28947        assert_eq!(c.column, "calendar_id");
28948        assert_eq!(
28949            c.extra_columns,
28950            vec!["uid".to_string(), "recurrence_id".to_string()]
28951        );
28952        assert!(c.partial_predicate.is_some());
28953    }
28954
28955    #[test]
28956    fn create_unique_index_using_btree_ok() {
28957        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
28958        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
28959    }
28960
28961    #[test]
28962    fn create_unique_index_using_hnsw_rejected() {
28963        let err =
28964            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
28965        assert!(err.message.contains("UNIQUE"), "{}", err.message);
28966    }
28967
28968    #[test]
28969    fn create_unique_index_round_trip() {
28970        let original = parse(
28971            "CREATE UNIQUE INDEX uq_calendar_events_master \
28972             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
28973        );
28974        let again = parse_statement(&original.to_string()).unwrap();
28975        assert_eq!(original, again);
28976    }
28977
28978    #[test]
28979    fn create_unique_without_index_errors() {
28980        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
28981        // v7.39 (round 340, V56) — PG 18.4, verbatim.
28982        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
28983    }
28984
28985    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
28986
28987    #[test]
28988    fn create_table_bytea_column() {
28989        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
28990        let Statement::CreateTable(c) = s else {
28991            panic!("expected CreateTable");
28992        };
28993        assert_eq!(c.columns.len(), 2);
28994        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
28995        assert!(!c.columns[1].nullable);
28996    }
28997
28998    #[test]
28999    fn create_table_bytes_alias_column() {
29000        let s = parse("CREATE TABLE t (blob BYTES)");
29001        let Statement::CreateTable(c) = s else {
29002            panic!("expected CreateTable");
29003        };
29004        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
29005    }
29006
29007    #[test]
29008    fn bytea_round_trip_display() {
29009        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
29010        let again = parse_statement(&original.to_string()).unwrap();
29011        assert_eq!(original, again);
29012    }
29013
29014    // --- v0.9 transactions -------------------------------------------------
29015
29016    #[test]
29017    fn begin_commit_rollback_parse_as_unit_variants() {
29018        let plain = crate::ast::TransactionModes::default();
29019        assert_eq!(parse("BEGIN"), Statement::Begin(plain));
29020        assert_eq!(parse("COMMIT"), Statement::Commit);
29021        // r1066 — PG synonyms pgbench's tpcb script relies on.
29022        assert_eq!(parse("END"), Statement::Commit);
29023        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
29024        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
29025        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
29026        // Trailing semicolons accepted too.
29027        assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
29028        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
29029        // statement (with or without the WORK/TRANSACTION noise word).
29030        assert_eq!(
29031            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
29032            Statement::Begin(crate::ast::TransactionModes {
29033                isolation: Some(IsolationLevel::RepeatableRead),
29034                read_only: None,
29035            })
29036        );
29037        assert_eq!(
29038            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29039            Statement::Begin(crate::ast::TransactionModes {
29040                isolation: Some(IsolationLevel::Serializable),
29041                read_only: None,
29042            })
29043        );
29044        // v7.39 — this line used to read
29045        //
29046        //     // A non-isolation mode keeps the session default (None).
29047        //     assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29048        //
29049        // which pinned the defect rather than catching it: the READ ONLY
29050        // was thrown away, so the statement opened an ordinary read-write
29051        // transaction and every write inside it was accepted. The
29052        // isolation level is still absent here, because this statement
29053        // does not name one — that part was right.
29054        assert_eq!(
29055            parse("BEGIN READ ONLY"),
29056            Statement::Begin(crate::ast::TransactionModes {
29057                isolation: None,
29058                read_only: Some(true),
29059            })
29060        );
29061        assert_eq!(
29062            parse("START TRANSACTION READ WRITE"),
29063            Statement::Begin(crate::ast::TransactionModes {
29064                isolation: None,
29065                read_only: Some(false),
29066            })
29067        );
29068        assert_eq!(
29069            parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29070            Statement::Begin(crate::ast::TransactionModes {
29071                isolation: Some(IsolationLevel::Serializable),
29072                read_only: Some(true),
29073            })
29074        );
29075    }
29076
29077    // --- v1.2: pgvector distance ops + ::vector cast --------------------
29078
29079    #[test]
29080    fn inner_product_binop_parses() {
29081        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29082        let Statement::Select(s) = s else { panic!() };
29083        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29084            panic!()
29085        };
29086        assert!(matches!(
29087            expr,
29088            Expr::Binary {
29089                op: BinOp::InnerProduct,
29090                ..
29091            }
29092        ));
29093    }
29094
29095    #[test]
29096    fn cosine_distance_binop_parses() {
29097        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29098        let Statement::Select(s) = s else { panic!() };
29099        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29100            panic!()
29101        };
29102        assert!(matches!(
29103            expr,
29104            Expr::Binary {
29105                op: BinOp::CosineDistance,
29106                ..
29107            }
29108        ));
29109    }
29110
29111    #[test]
29112    fn vector_cast_postfix_wraps_string_literal() {
29113        let s = parse("SELECT '[1,2,3]'::vector FROM t");
29114        let Statement::Select(s) = s else { panic!() };
29115        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29116            panic!()
29117        };
29118        assert!(matches!(
29119            expr,
29120            Expr::Cast {
29121                target: CastTarget::Vector,
29122                ..
29123            }
29124        ));
29125    }
29126
29127    #[test]
29128    fn unsupported_cast_target_errors() {
29129        // v7.37.5 ship triage promoted the parser to accept every
29130        // ident as a `CastTarget::Named(canonical)`; the engine
29131        // surfaces the "unsupported cast target" error at eval
29132        // time when `type_name_to_data_type` can't resolve it.
29133        // Parser-side error now requires a NON-ident after `::`
29134        // (e.g. a punctuation token).
29135        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29136        assert_eq!(err.message, "syntax error at or near \",\"");
29137    }
29138
29139    #[test]
29140    fn tx_statements_round_trip() {
29141        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29142            let original = parse(q);
29143            let again = parse_statement(&original.to_string()).unwrap();
29144            assert_eq!(original, again);
29145        }
29146    }
29147
29148    #[test]
29149    fn interval_text_parsing_units() {
29150        // v7.37.5 β — three-field shape `(months, days, micros)` so
29151        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29152        // Single unit.
29153        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29154        assert_eq!(
29155            parse_interval_text("24 hours"),
29156            Some((0, 0, 86_400_000_000))
29157        );
29158        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29159        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29160        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29161        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29162        // Compound spans accumulate per-dimension.
29163        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29164        assert_eq!(
29165            parse_interval_text("1 day 2 hours"),
29166            Some((0, 1, 7_200_000_000))
29167        );
29168        // Negative numbers carry through per-dimension.
29169        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29170        // Bad shapes return None.
29171        assert_eq!(parse_interval_text(""), None);
29172        assert_eq!(parse_interval_text("garbage"), None);
29173        assert_eq!(parse_interval_text("1 fortnight"), None);
29174        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29175        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29176        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29177        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29178        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29179    }
29180
29181    #[test]
29182    fn interval_literal_roundtrips_via_display() {
29183        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29184        let s = parsed.to_string();
29185        // Display preserves the original text verbatim.
29186        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29187        // And re-parsing yields a structurally equal statement.
29188        let again = parse_statement(&s).unwrap();
29189        assert_eq!(parsed, again);
29190    }
29191
29192    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29193
29194    #[test]
29195    fn parser_recognises_create_publication_bare() {
29196        let s = parse("CREATE PUBLICATION pub_a");
29197        let Statement::CreatePublication(p) = s else {
29198            panic!("expected CreatePublication, got {s:?}")
29199        };
29200        assert_eq!(p.name, "pub_a");
29201        assert_eq!(p.scope, PublicationScope::AllTables);
29202    }
29203
29204    #[test]
29205    fn parser_recognises_create_publication_for_all_tables() {
29206        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29207        let Statement::CreatePublication(p) = s else {
29208            panic!("expected CreatePublication, got {s:?}")
29209        };
29210        assert_eq!(p.name, "pub_a");
29211        assert_eq!(p.scope, PublicationScope::AllTables);
29212    }
29213
29214    #[test]
29215    fn parser_recognises_drop_publication() {
29216        let s = parse("DROP PUBLICATION pub_a");
29217        let Statement::DropPublication { name, .. } = s else {
29218            panic!("expected DropPublication, got {s:?}")
29219        };
29220        assert_eq!(name, "pub_a");
29221    }
29222
29223    #[test]
29224    fn parser_recognises_for_table_list() {
29225        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29226        let Statement::CreatePublication(p) = s else {
29227            panic!("expected CreatePublication, got {s:?}")
29228        };
29229        assert_eq!(p.name, "pub_a");
29230        let PublicationScope::ForTables(ts) = p.scope else {
29231            panic!("expected ForTables scope")
29232        };
29233        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29234    }
29235
29236    #[test]
29237    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29238        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29239        // is rejected (`invalid publication object list`; the old
29240        // test pinned an unverifiable "PG 19 accepts both" claim);
29241        // TABLES pairs with IN SCHEMA.
29242        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29243            .expect_err("bare FOR TABLES must reject");
29244        assert!(
29245            alloc::format!("{err}").contains("invalid publication object list"),
29246            "got: {err}"
29247        );
29248        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29249        let Statement::CreatePublication(p) = s else {
29250            panic!("expected CreatePublication, got {s:?}")
29251        };
29252        let PublicationScope::TablesInSchema(schema) = p.scope else {
29253            panic!("expected TablesInSchema")
29254        };
29255        assert_eq!(schema, "public");
29256    }
29257
29258    #[test]
29259    fn parser_recognises_for_all_tables_except_list() {
29260        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29261        let Statement::CreatePublication(p) = s else {
29262            panic!()
29263        };
29264        let PublicationScope::AllTablesExcept(ts) = p.scope else {
29265            panic!("expected AllTablesExcept")
29266        };
29267        assert_eq!(ts, alloc::vec!["t1", "t2"]);
29268    }
29269
29270    #[test]
29271    fn parser_rejects_for_table_with_empty_list() {
29272        // `FOR TABLE` with nothing after is a parse error.
29273        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29274            .expect_err("must error on empty list");
29275        // No specific message asserted — the call falls through to
29276        // expect_ident_like which yields "expected identifier, got …".
29277        assert!(!err.message.is_empty());
29278    }
29279
29280    #[test]
29281    fn parser_recognises_show_publications() {
29282        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29283        // bare ident in this position, NOT a reserved keyword.
29284        let s = parse("SHOW PUBLICATIONS");
29285        assert!(matches!(s, Statement::ShowPublications));
29286    }
29287
29288    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29289
29290    #[test]
29291    fn parser_recognises_create_subscription_single_publication() {
29292        let s = parse(
29293            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29294        );
29295        let Statement::CreateSubscription(c) = s else {
29296            panic!("expected CreateSubscription, got {s:?}")
29297        };
29298        assert_eq!(c.name, "sub_a");
29299        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29300        assert_eq!(c.publications, alloc::vec!["pub_a"]);
29301    }
29302
29303    #[test]
29304    fn parser_recognises_create_subscription_multi_publication() {
29305        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29306        let Statement::CreateSubscription(c) = s else {
29307            panic!()
29308        };
29309        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29310    }
29311
29312    #[test]
29313    fn parser_rejects_create_subscription_missing_connection() {
29314        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29315            .expect_err("must error on missing CONNECTION");
29316        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29317    }
29318
29319    #[test]
29320    fn parser_rejects_create_subscription_missing_publication() {
29321        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29322            .expect_err("must error on missing PUBLICATION");
29323        assert_eq!(err.message, "syntax error at end of input");
29324    }
29325
29326    #[test]
29327    fn parser_recognises_drop_subscription() {
29328        let s = parse("DROP SUBSCRIPTION sub_a");
29329        let Statement::DropSubscription { name, .. } = s else {
29330            panic!("expected DropSubscription, got {s:?}")
29331        };
29332        assert_eq!(name, "sub_a");
29333    }
29334
29335    #[test]
29336    fn parser_recognises_show_subscriptions() {
29337        let s = parse("SHOW SUBSCRIPTIONS");
29338        assert!(matches!(s, Statement::ShowSubscriptions));
29339    }
29340
29341    #[test]
29342    fn parser_recognises_wait_for_wal_position_no_timeout() {
29343        let s = parse("WAIT FOR WAL POSITION 12345");
29344        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29345            panic!("expected WaitForWalPosition, got {s:?}")
29346        };
29347        assert_eq!(pos, 12345);
29348        assert!(timeout_ms.is_none());
29349    }
29350
29351    #[test]
29352    fn parser_recognises_wait_for_wal_position_with_timeout() {
29353        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29354        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29355            panic!()
29356        };
29357        assert_eq!(pos, 67890);
29358        assert_eq!(timeout_ms, Some(5000));
29359    }
29360
29361    #[test]
29362    fn parser_rejects_wait_with_negative_position() {
29363        // The lexer treats `-` as a token; `expect_u64_literal`
29364        // only sees the Integer that follows, so the negative
29365        // arrives as a unary-minus expression at higher levels.
29366        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29367        // parse error one way or another.
29368        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29369        assert!(!err.message.is_empty());
29370    }
29371
29372    #[test]
29373    fn parser_recognises_bare_analyze() {
29374        let s = parse("ANALYZE");
29375        assert!(matches!(s, Statement::Analyze(None)));
29376    }
29377
29378    #[test]
29379    fn parser_recognises_analyze_with_table() {
29380        let s = parse("ANALYZE users");
29381        let Statement::Analyze(Some(name)) = s else {
29382            panic!("expected Analyze, got {s:?}")
29383        };
29384        assert_eq!(name, "users");
29385    }
29386
29387    #[test]
29388    fn parser_recognises_analyze_with_quoted_table() {
29389        let s = parse("ANALYZE \"Mixed Case\"");
29390        let Statement::Analyze(Some(name)) = s else {
29391            panic!()
29392        };
29393        assert_eq!(name, "Mixed Case");
29394    }
29395
29396    #[test]
29397    fn parser_rejects_analyze_with_garbage_token() {
29398        let err = parse_statement("ANALYZE 42").expect_err("must error");
29399        assert!(!err.message.is_empty());
29400    }
29401
29402    #[test]
29403    fn analyze_display_roundtrips() {
29404        for sql in ["ANALYZE", "ANALYZE users"] {
29405            let s = parse(sql);
29406            let printed = s.to_string();
29407            let again = parse_statement(&printed)
29408                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29409            assert_eq!(s, again);
29410        }
29411    }
29412
29413    #[test]
29414    fn wait_for_display_roundtrips() {
29415        for sql in [
29416            "WAIT FOR WAL POSITION 12345",
29417            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
29418        ] {
29419            let s = parse(sql);
29420            let printed = s.to_string();
29421            let again = parse_statement(&printed)
29422                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29423            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29424        }
29425    }
29426
29427    #[test]
29428    fn subscription_ddl_display_roundtrips() {
29429        for sql in [
29430            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
29431            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
29432            "DROP SUBSCRIPTION sub_a",
29433            "SHOW SUBSCRIPTIONS",
29434        ] {
29435            let s = parse(sql);
29436            let printed = s.to_string();
29437            let again = parse_statement(&printed)
29438                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29439            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29440        }
29441    }
29442
29443    #[test]
29444    fn parser_drop_dispatches_user_vs_publication() {
29445        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
29446        // tokenises DROP. Both targets must still parse.
29447        let s = parse("DROP USER 'alice'");
29448        let Statement::DropUser { name, .. } = s else {
29449            panic!("expected DropUser, got {s:?}")
29450        };
29451        assert_eq!(name, "alice");
29452        // And DROP PUBLICATION lands the new variant.
29453        let s = parse("DROP PUBLICATION p1");
29454        assert!(matches!(s, Statement::DropPublication { .. }));
29455    }
29456
29457    #[test]
29458    fn publication_ddl_display_roundtrips() {
29459        // Every CREATE PUBLICATION variant must Display → parse →
29460        // same AST. v6.1.3 covers all three scope shapes.
29461        for sql in [
29462            "CREATE PUBLICATION pub_a",
29463            "CREATE PUBLICATION pub_a FOR ALL TABLES",
29464            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29465            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29466            "DROP PUBLICATION pub_a",
29467            "SHOW PUBLICATIONS",
29468        ] {
29469            let s = parse(sql);
29470            let printed = s.to_string();
29471            let again = parse_statement(&printed)
29472                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29473            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29474        }
29475    }
29476
29477    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29478
29479    #[test]
29480    fn create_function_returns_trigger_plpgsql_minimal() {
29481        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29482        let s = parse(sql);
29483        let Statement::CreateFunction(f) = s else {
29484            panic!("expected CreateFunction");
29485        };
29486        assert_eq!(f.name, "noop");
29487        assert!(!f.or_replace);
29488        assert!(f.args.is_empty());
29489        assert!(matches!(f.returns, FunctionReturn::Trigger));
29490        assert_eq!(f.language, "plpgsql");
29491        let FunctionBody::PlPgSql(block) = f.body else {
29492            panic!("expected PlPgSql body");
29493        };
29494        assert_eq!(block.statements.len(), 1);
29495        assert!(matches!(
29496            block.statements[0],
29497            PlPgSqlStmt::Return(ReturnTarget::New)
29498        ));
29499    }
29500
29501    #[test]
29502    fn create_function_or_replace_with_assignment() {
29503        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
29504        // RETURN NEW.
29505        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
29506BEGIN
29507  NEW.search_vector := to_tsvector('english', NEW.subject);
29508  RETURN NEW;
29509END;
29510$$";
29511        let s = parse(sql);
29512        let Statement::CreateFunction(f) = s else {
29513            panic!("expected CreateFunction");
29514        };
29515        assert!(f.or_replace);
29516        let FunctionBody::PlPgSql(block) = &f.body else {
29517            panic!("expected PlPgSql body");
29518        };
29519        assert_eq!(block.statements.len(), 2);
29520        // First statement: NEW.search_vector := to_tsvector(...)
29521        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
29522            panic!("expected Assign as first stmt");
29523        };
29524        match target {
29525            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
29526            other => panic!("expected NEW.col, got {other:?}"),
29527        }
29528        // Second statement: RETURN NEW
29529        assert!(matches!(
29530            block.statements[1],
29531            PlPgSqlStmt::Return(ReturnTarget::New)
29532        ));
29533    }
29534
29535    #[test]
29536    fn create_trigger_after_insert_or_update() {
29537        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
29538        let s = parse(sql);
29539        let Statement::CreateTrigger(t) = s else {
29540            panic!("expected CreateTrigger");
29541        };
29542        assert_eq!(t.name, "tg");
29543        assert_eq!(t.table, "messages");
29544        assert_eq!(t.timing, TriggerTiming::After);
29545        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
29546        assert_eq!(t.for_each, TriggerForEach::Row);
29547        assert_eq!(t.function, "update_sv");
29548    }
29549
29550    #[test]
29551    fn create_trigger_before_delete_execute_procedure_alias() {
29552        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
29553        let sql =
29554            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
29555        let s = parse(sql);
29556        let Statement::CreateTrigger(t) = s else {
29557            panic!("expected CreateTrigger");
29558        };
29559        assert_eq!(t.timing, TriggerTiming::Before);
29560        assert_eq!(t.events, vec![TriggerEvent::Delete]);
29561    }
29562
29563    #[test]
29564    fn drop_trigger_if_exists_round_trips() {
29565        // No parser support for DROP TRIGGER yet — added in v7.12.5
29566        // alongside the broader DROP …{IF EXISTS} cleanup. The
29567        // AST + Display impls are in place so we round-trip via
29568        // construction:
29569        let s = Statement::DropTrigger {
29570            name: "tg".into(),
29571            table: "messages".into(),
29572            if_exists: true,
29573        };
29574        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
29575    }
29576
29577    #[test]
29578    fn trigger_ddl_display_roundtrips_through_parser() {
29579        // CREATE TRIGGER + its referenced CREATE FUNCTION must
29580        // Display → parse → same AST (modulo PL/pgSQL body
29581        // formatting which is parser-canonicalised).
29582        for sql in [
29583            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
29584            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
29585        ] {
29586            let s = parse(sql);
29587            let printed = s.to_string();
29588            let again = parse_statement(&printed)
29589                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29590            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29591        }
29592    }
29593}