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            // v7.39.9 — MySQL's top-level `RENAME TABLE a TO b [, c TO d]`.
3666            //
3667            // PostgreSQL renames a table through `ALTER TABLE … RENAME
3668            // TO`, which SPG already had, so this spelling answered 1064
3669            // — and it is what a MySQL migration writes. Measured on
3670            // 9.7.2: several pairs in one statement are accepted, and
3671            // renaming onto a name that exists is 1050.
3672            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename") => {
3673                self.advance();
3674                if matches!(self.peek(), Token::Table) {
3675                    self.advance();
3676                }
3677                let mut pairs: Vec<(String, String)> = Vec::new();
3678                loop {
3679                    let from = self.expect_ident_like()?;
3680                    if matches!(self.peek(), Token::To) {
3681                        self.advance();
3682                    } else {
3683                        self.expect_keyword_ident("to")?;
3684                    }
3685                    let to = self.expect_ident_like()?;
3686                    pairs.push((from, to));
3687                    if matches!(self.peek(), Token::Comma) {
3688                        self.advance();
3689                    } else {
3690                        break;
3691                    }
3692                }
3693                Ok(Statement::RenameTables(pairs))
3694            }
3695            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3696                self.advance();
3697                // Optional TABLE noise word — PG accepts both the reserved
3698                // token and the bare identifier spelling.
3699                if matches!(self.peek(), Token::Table)
3700                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3701                {
3702                    self.advance();
3703                }
3704                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3705                // not absorbed. The lookahead keeps a table genuinely
3706                // called `only` working: the keyword is a keyword only
3707                // when a name follows it.
3708                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3709                    if s.eq_ignore_ascii_case("only"))
3710                    && matches!(
3711                        self.tokens.get(self.pos + 1),
3712                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3713                    );
3714                if only {
3715                    self.advance();
3716                }
3717                // Table names (comma-separated).
3718                let mut tables = Vec::new();
3719                loop {
3720                    tables.push(self.expect_ident_like()?);
3721                    if matches!(self.peek(), Token::Comma) {
3722                        self.advance();
3723                        continue;
3724                    }
3725                    break;
3726                }
3727                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3728                let mut restart_identity = false;
3729                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3730                {
3731                    self.advance();
3732                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3733                    {
3734                        self.advance();
3735                        restart_identity = true;
3736                    }
3737                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3738                {
3739                    self.advance();
3740                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3741                    {
3742                        self.advance();
3743                    }
3744                }
3745                // Optional CASCADE / RESTRICT.
3746                let mut cascade = false;
3747                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3748                {
3749                    self.advance();
3750                    cascade = true;
3751                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3752                {
3753                    self.advance();
3754                }
3755                Ok(Statement::Truncate {
3756                    tables,
3757                    restart_identity,
3758                    cascade,
3759                    only,
3760                })
3761            }
3762            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3763            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3764            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3765            // rows change so the index tree is always up-to-date;
3766            // REINDEX is a strict no-op. Accept the whole statement
3767            // shape to boundary for pg_dump round-trip compatibility.
3768            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3769                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3770                // index bloat to rebuild, so the work stays a no-op, but PG
3771                // validates what it was pointed at and this swallowed the
3772                // name at parse time — `REINDEX TABLE typo` reported
3773                // success. Measured on PG18: INDEX / TABLE name a relation,
3774                // SCHEMA a schema, SYSTEM nothing.
3775                self.advance();
3776                self.parse_reindex_tail()
3777            }
3778            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3779            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3780            // SPG has no MVCC bloat today (Phase D visibility map
3781            // queues with v7.38); the freezer collapses hot-tier
3782            // rows into cold segments automatically. VACUUM is a
3783            // no-op — pg_dump maintenance scripts and Discourse's
3784            // periodic-maintenance path both emit it.
3785            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3786            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3787            // actual bloat, so the pre-MVCC accept-and-ignore posture
3788            // became a silent no-op on a customer's manual reclaim.
3789            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3790            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3791            // ANALYZE is captured, the optional table name is captured.
3792            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3793                self.advance();
3794                // Parenthesised option list: absorb it.
3795                if matches!(self.peek(), Token::LParen) {
3796                    let mut depth = 0usize;
3797                    loop {
3798                        match self.advance() {
3799                            Token::LParen => depth += 1,
3800                            Token::RParen => {
3801                                depth -= 1;
3802                                if depth == 0 {
3803                                    break;
3804                                }
3805                            }
3806                            Token::Eof => break,
3807                            _ => {}
3808                        }
3809                    }
3810                }
3811                let mut analyze = false;
3812                let mut table: Option<String> = None;
3813                loop {
3814                    match self.peek() {
3815                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3816                        // an identifier, so the loop below broke out on it and
3817                        // dropped the table name: `VACUUM FULL nosuch` was
3818                        // accepted where `VACUUM nosuch` was refused.
3819                        Token::Full => {
3820                            self.advance();
3821                        }
3822                        Token::Ident(w) | Token::QuotedIdent(w) => {
3823                            let wl = w.to_ascii_lowercase();
3824                            match wl.as_str() {
3825                                "full" | "freeze" | "verbose" => {
3826                                    self.advance();
3827                                }
3828                                "analyze" | "analyse" => {
3829                                    analyze = true;
3830                                    self.advance();
3831                                }
3832                                _ => {
3833                                    table = Some(self.expect_ident_like()?);
3834                                    break;
3835                                }
3836                            }
3837                        }
3838                        _ => break,
3839                    }
3840                }
3841                // Optional trailing column list / anything else to the
3842                // statement boundary (PG accepts per-column ANALYZE).
3843                self.consume_until_statement_boundary();
3844                Ok(Statement::Vacuum { table, analyze })
3845            }
3846            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3847            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3848            // <index>. PG stores rows in physical order matching
3849            // an index; SPG's hot-tier is append-only + cold-tier
3850            // is segment-frozen, so clustering has no persistent
3851            // effect. Accept-and-no-op for pg_dump compat.
3852            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3853                // v7.39 (round 535) — same as REINDEX above: the relation is
3854                // carried so the engine can refuse one that does not exist.
3855                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3856                self.advance();
3857                self.parse_cluster_tail()
3858            }
3859            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3860            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3861            // optional string payload; UNLISTEN takes a channel or `*`.
3862            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3863                self.advance();
3864                let ch = match self.advance() {
3865                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3866                    other => {
3867                        return Err(self.err(format!(
3868                            "expected channel name after LISTEN, got {other:?}"
3869                        )));
3870                    }
3871                };
3872                Ok(Statement::Listen(ch))
3873            }
3874            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3875                self.advance();
3876                let channel = match self.advance() {
3877                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3878                    other => {
3879                        return Err(self.err(format!(
3880                            "expected channel name after NOTIFY, got {other:?}"
3881                        )));
3882                    }
3883                };
3884                let payload = if matches!(self.peek(), Token::Comma) {
3885                    self.advance();
3886                    match self.advance() {
3887                        Token::String(p) => Some(p),
3888                        other => {
3889                            return Err(self.err(format!(
3890                                "expected string payload after NOTIFY <channel>, got {other:?}"
3891                            )));
3892                        }
3893                    }
3894                } else {
3895                    None
3896                };
3897                Ok(Statement::Notify { channel, payload })
3898            }
3899            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3900                self.advance();
3901                match self.advance() {
3902                    Token::Star => Ok(Statement::Unlisten(None)),
3903                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3904                    other => Err(self.err(format!(
3905                        "expected channel name or * after UNLISTEN, got {other:?}"
3906                    ))),
3907                }
3908            }
3909            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3910            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3911            // process-wide write lock today; explicit LOCK has no
3912            // effect. Accept-and-no-op for pg_dump / migration
3913            // compat.
3914            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3915                self.advance();
3916                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3917                // engine holds a process-wide write lock), but the TABLE
3918                // NAME is now carried out so the engine can refuse one that
3919                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3920                // READ|WRITE` is a different statement with the same first
3921                // word; it keeps the old no-op, because a MySQL dump's
3922                // bracket names tables it is about to create.
3923                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3924                    if k.eq_ignore_ascii_case("tables"));
3925                if mysql_tables {
3926                    self.consume_until_statement_boundary();
3927                    return Ok(Statement::Empty);
3928                }
3929                if matches!(self.peek(), Token::Table) {
3930                    self.advance();
3931                }
3932                let names = self.take_comma_separated_names();
3933                self.consume_until_statement_boundary();
3934                Ok(Statement::ValidateOnly {
3935                    kind: crate::ast::ValidateOnlyKind::LockTable,
3936                    names,
3937                })
3938            }
3939            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3940            // durability marker + snapshot in PG. SPG has WAL
3941            // checkpointing on a byte / time schedule (v7.37.10
3942            // 60s / 4 MiB defaults). The bare statement parses to
3943            // `Statement::Empty` here (the no_std engine owns no
3944            // WAL / snapshot); v7.37 Epic Du wires the HOST
3945            // (embedded `Database::execute_buffered`, via
3946            // `sql_is_checkpoint`) to force an immediate synchronous
3947            // checkpoint through `Database::checkpoint` — a real
3948            // durability barrier, matching PG.
3949            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3950                self.advance();
3951                self.consume_until_statement_boundary();
3952                Ok(Statement::Empty)
3953            }
3954            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3955                self.advance();
3956                self.parse_delete_after_keyword()
3957            }
3958            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3959            // ALTER is not a reserved keyword in the lexer — handled
3960            // as a bare ident here.
3961            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3962                self.advance();
3963                self.parse_alter_after_keyword()
3964            }
3965            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3966            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3967            // additions needed.
3968            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3969                self.advance();
3970                self.parse_wait_after_keyword()
3971            }
3972            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3973            // Bare ANALYZE → analyse every user table; ANALYZE
3974            // <name> → re-stats one. The argument is an optional
3975            // ident (or quoted ident); anything else is a parse
3976            // error.
3977            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
3978            // `WHERE` filter (carved out per V6_7_DESIGN.md
3979            // STABILITY). Lex order: identifier "compact" → "cold"
3980            // → "segments". Anything else after `COMPACT` is a
3981            // parse error.
3982            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
3983                self.advance();
3984                let next = self.peek().clone();
3985                let cold = match next {
3986                    Token::Ident(s) | Token::QuotedIdent(s) => s,
3987                    _ => {
3988                        return Err(
3989                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
3990                        );
3991                    }
3992                };
3993                if !cold.eq_ignore_ascii_case("cold") {
3994                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
3995                }
3996                self.advance();
3997                let next = self.peek().clone();
3998                let segments = match next {
3999                    Token::Ident(s) | Token::QuotedIdent(s) => s,
4000                    _ => {
4001                        return Err(self.err(format!(
4002                            "expected SEGMENTS after COMPACT COLD, got {:?}",
4003                            self.peek()
4004                        )));
4005                    }
4006                };
4007                if !segments.eq_ignore_ascii_case("segments") {
4008                    return Err(self.err(format!(
4009                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
4010                    )));
4011                }
4012                self.advance();
4013                Ok(Statement::CompactColdSegments)
4014            }
4015            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
4016            // Parsed as a case-insensitive identifier since MERGE
4017            // isn't a reserved lexer keyword (collides with the
4018            // mysqldump `ALGORITHM = MERGE` view clause if it
4019            // were); the inner parser drives the rest of the
4020            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
4021            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
4022                self.advance();
4023                self.parse_merge_after_keyword()
4024            }
4025            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
4026                self.advance();
4027                // v7.39.9 — MySQL spells it `ANALYZE TABLE t`. The
4028                // keyword is noise to the parse; what differs is the
4029                // ANSWER, which MySQL returns as a result set — see the
4030                // executor.
4031                let mysql_table_kw = matches!(self.peek(), Token::Table);
4032                if mysql_table_kw {
4033                    self.advance();
4034                }
4035                let target = match self.peek() {
4036                    Token::Eof | Token::Semicolon => None,
4037                    Token::Ident(_) | Token::QuotedIdent(_) => {
4038                        Some(self.expect_ident_like()?)
4039                    }
4040                    other => {
4041                        return Err(self.err(format!(
4042                            "expected table name or end of statement after ANALYZE, got {other:?}"
4043                        )));
4044                    }
4045                };
4046                // v7.39 (round 776, F31 J7) — the per-column form
4047                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
4048                // here while the VACUUM arm already consumed it; SPG
4049                // analyzes whole tables, so the list parses and is
4050                // accepted like the VACUUM path's.
4051                if target.is_some() && matches!(self.peek(), Token::LParen) {
4052                    self.advance();
4053                    loop {
4054                        let _ = self.expect_ident_like()?;
4055                        match self.peek() {
4056                            Token::Comma => {
4057                                self.advance();
4058                            }
4059                            Token::RParen => {
4060                                self.advance();
4061                                break;
4062                            }
4063                            other => {
4064                                return Err(self.err(format!(
4065                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
4066                                )));
4067                            }
4068                        }
4069                    }
4070                }
4071                Ok(Statement::Analyze(target))
4072            }
4073            // v7.12.1 — `SET <name> [TO|=] <value>`. The
4074            // `default_text_search_config` parameter is consumed
4075            // by the FTS function dispatcher; other parameter
4076            // names are recorded but treated as a no-op so PG
4077            // dump output loads.
4078            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4079                self.advance();
4080                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4081                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4082                // …` which the SessionVar path handles). `LOCAL` is the only
4083                // one that changes semantics — it scopes the change to the
4084                // current transaction — so capture it; SESSION / GLOBAL are
4085                // accepted and treated as the default session scope.
4086                let mut set_local = false;
4087                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4088                    let q = s.to_ascii_lowercase();
4089                    if q == "local" || q == "session" || q == "global" {
4090                        set_local = q == "local";
4091                        self.advance();
4092                    }
4093                }
4094                // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4095                // { DEFAULT | <role> }`. pg_dump's ACL section switches
4096                // to the object owner with it. SPG maps it onto the
4097                // session-role machinery (recorded delta RD-10: PG moves
4098                // session_user too; SPG moves the effective role).
4099                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4100                    if s.eq_ignore_ascii_case("authorization"))
4101                {
4102                    self.advance(); // AUTHORIZATION
4103                    let role = match self.peek().clone() {
4104                        Token::Default => {
4105                            self.advance();
4106                            None
4107                        }
4108                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4109                            self.advance();
4110                            Some(s)
4111                        }
4112                        _ => None,
4113                    };
4114                    return Ok(Statement::SetRole(role));
4115                }
4116                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4117                // <collation>]` — change the connection client
4118                // charset. SPG stores UTF-8 always and orders
4119                // bytewise; accept as a no-op.
4120                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4121                {
4122                    self.advance();
4123                    // v7.39 — this used to parse the clause and throw it
4124                    // away ("SPG stores UTF-8 always and orders
4125                    // bytewise; accept as a no-op"). That sentence
4126                    // stopped being true when collations arrived, and
4127                    // once `collation_connection` began driving literal
4128                    // comparison, dropping the COLLATE clause became a
4129                    // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4130                    // utf8mb4_general_ci` reported back
4131                    // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4132                    //
4133                    // The charset name is emitted as `names` and the
4134                    // ENGINE expands it, because which collation a
4135                    // charset defaults to is MySQL semantics and belongs
4136                    // beside the rest of them, not in the parser.
4137                    let mut pairs = alloc::vec::Vec::new();
4138                    if matches!(
4139                        self.peek(),
4140                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4141                    ) {
4142                        let charset = match self.advance() {
4143                            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4144                            _ => unreachable!("peeked an ident-or-string"),
4145                        };
4146                        pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4147                    }
4148                    // Optional `COLLATE <name>` — emitted AFTER `names`
4149                    // so it overrides the charset's default, which is
4150                    // what MySQL does.
4151                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4152                    {
4153                        self.advance();
4154                        if matches!(
4155                            self.peek(),
4156                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4157                        ) {
4158                            let coll = match self.advance() {
4159                                Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4160                                _ => unreachable!("peeked an ident-or-string"),
4161                            };
4162                            pairs.push((
4163                                String::from("collation_connection"),
4164                                crate::ast::SetValue::Ident(coll),
4165                            ));
4166                        }
4167                    }
4168                    if pairs.is_empty() {
4169                        return Ok(Statement::Empty);
4170                    }
4171                    return Ok(Statement::SetParameterList(pairs));
4172                }
4173                // v7.37.17 (17.6 sibling) — PG `SET ROLE
4174                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4175                // uses this to switch to the object owner before
4176                // recreating tables. SPG has no role system so this
4177                // is a no-op.
4178                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4179                {
4180                    self.advance(); // ROLE
4181                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4182                    // reset to the login identity; a name / string sets the
4183                    // effective role that drives current_user + RLS.
4184                    let role = match self.peek().clone() {
4185                        Token::Default => {
4186                            self.advance();
4187                            None
4188                        }
4189                        Token::Ident(s) | Token::QuotedIdent(s)
4190                            if s.eq_ignore_ascii_case("none") =>
4191                        {
4192                            self.advance();
4193                            None
4194                        }
4195                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4196                            self.advance();
4197                            Some(s)
4198                        }
4199                        _ => None,
4200                    };
4201                    return Ok(Statement::SetRole(role));
4202                }
4203                // v7.37.17 (17.6 sibling) — PG `SET SESSION
4204                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4205                // ISO SQL surface). pg_dump prepends this to fix
4206                // the isolation level for the restore session. SPG
4207                // defaults to READ COMMITTED and doesn't yet honor
4208                // session-set isolation across statements — accept
4209                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4210                // per-tx form is handled elsewhere.
4211                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4212                {
4213                    self.advance(); // CHARACTERISTICS
4214                    if matches!(self.peek(), Token::As) {
4215                        self.advance();
4216                    }
4217                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4218                        self.advance();
4219                    }
4220                    // v7.39 — no longer a no-op. The note above said SPG
4221                    // "doesn't yet honor session-set isolation across
4222                    // statements"; it does now, through
4223                    // `default_transaction_isolation`, and measured on
4224                    // PG 18.6 this statement is exactly a way to set it:
4225                    //
4226                    //   SET SESSION CHARACTERISTICS AS TRANSACTION
4227                    //       ISOLATION LEVEL REPEATABLE READ;
4228                    //   current_setting('default_transaction_isolation')
4229                    //       -> repeatable read
4230                    //
4231                    // pg_dump prepends this to fix the level for a
4232                    // restore session, so accepting it and doing nothing
4233                    // meant the restore ran at a level nobody chose.
4234                    //
4235                    // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4236                    // still consumed and dropped. `default_transaction_read_only`
4237                    // exists in the GUC inventory but nothing enforces it,
4238                    // and setting a value no code honours is the very
4239                    // defect this version is about — a session told it
4240                    // holds a guarantee it does not.
4241                    let modes = self.parse_isolation_level_clauses()?;
4242                    self.consume_until_statement_boundary();
4243                    let mut pairs: alloc::vec::Vec<(
4244                        alloc::string::String,
4245                        crate::ast::SetValue,
4246                    )> = alloc::vec::Vec::new();
4247                    if let Some(level) = modes.isolation {
4248                        pairs.push((
4249                            alloc::string::String::from("default_transaction_isolation"),
4250                            crate::ast::SetValue::String(alloc::string::String::from(
4251                                level.as_pg_str(),
4252                            )),
4253                        ));
4254                    }
4255                    if let Some(ro) = modes.read_only {
4256                        pairs.push((
4257                            alloc::string::String::from("default_transaction_read_only"),
4258                            crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4259                                "on"
4260                            } else {
4261                                "off"
4262                            })),
4263                        ));
4264                    }
4265                    return Ok(if pairs.is_empty() {
4266                        Statement::Empty
4267                    } else {
4268                        Statement::SetParameterList(pairs)
4269                    });
4270                }
4271                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4272                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4273                // pg_dump emits this to control the deferrability of
4274                // FK / UNIQUE constraints across a bulk restore. SPG
4275                // has no deferrable-constraint machinery today; the
4276                // FK checker is strict-immediate. Accept-and-no-op
4277                // for pg_dump round-trip compatibility.
4278                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4279                {
4280                    self.advance(); // CONSTRAINTS
4281                    // v7.39 (round 288) — no longer a no-op: the trailing
4282                    // DEFERRED / IMMEDIATE sets the transaction's timing.
4283                    // v7.39 (round 308, V29) — and the names are kept.
4284                    // They used to be skipped over on the way to the
4285                    // DEFERRED keyword, so a named form silently behaved
4286                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4287                    // every deferrable constraint in the transaction.
4288                    let mut names: alloc::vec::Vec<alloc::string::String> =
4289                        alloc::vec::Vec::new();
4290                    if matches!(self.peek(), Token::All) {
4291                        self.advance();
4292                    } else {
4293                        loop {
4294                            let mut n = self.expect_ident_like()?;
4295                            // A schema-qualified name (`public.fk_a`)
4296                            // identifies the same constraint; PG resolves
4297                            // it by the trailing segment.
4298                            while matches!(self.peek(), Token::Dot) {
4299                                self.advance();
4300                                n = self.expect_ident_like()?;
4301                            }
4302                            names.push(n);
4303                            if matches!(self.peek(), Token::Comma) {
4304                                self.advance();
4305                            } else {
4306                                break;
4307                            }
4308                        }
4309                    }
4310                    let deferred = match self.peek() {
4311                        Token::Ident(s) | Token::QuotedIdent(s)
4312                            if s.eq_ignore_ascii_case("deferred") =>
4313                        {
4314                            true
4315                        }
4316                        Token::Ident(s) | Token::QuotedIdent(s)
4317                            if s.eq_ignore_ascii_case("immediate") =>
4318                        {
4319                            false
4320                        }
4321                        other => {
4322                            return Err(self.err(alloc::format!(
4323                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4324                            )));
4325                        }
4326                    };
4327                    self.advance();
4328                    return Ok(Statement::SetConstraints { names, deferred });
4329                }
4330                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4331                // { DEFAULT | '<role>' | <ident> }` (mailrs
4332                // round-10 A.1). pg_dump preamble emits the
4333                // `DEFAULT` form to reset session authorization.
4334                //
4335                // v7.39 (round 697) — this said "SPG has no role system so
4336                // this is a strict no-op". SPG has had one since round 58;
4337                // the comment outlived it, and with it the reason a name
4338                // that is not a role was accepted here. It still switches
4339                // no authorization — what it does now is refuse a role
4340                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4341                // AUTHORIZATION` (handled by the RESET parser
4342                // elsewhere). Reference:
4343                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4344                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4345                {
4346                    self.advance(); // AUTHORIZATION
4347                    match self.peek().clone() {
4348                        Token::Default => {
4349                            self.advance();
4350                        }
4351                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4352                            self.advance();
4353                            return Ok(Statement::ValidateOnly {
4354                                kind: crate::ast::ValidateOnlyKind::RoleName,
4355                                names: alloc::vec![r],
4356                            });
4357                        }
4358                        other => {
4359                            return Err(self.err(alloc::format!(
4360                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4361                            )));
4362                        }
4363                    }
4364                    return Ok(Statement::Empty);
4365                }
4366                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4367                // ISOLATION LEVEL { READ COMMITTED | READ
4368                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4369                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4370                // PG-standard surface. v7.37.8 accepts the syntax
4371                // and tracks the selected level on
4372                // `Engine::current_isolation_level()`; the actual
4373                // MVCC / SSI semantics implementation lands in
4374                // the 轴 4 isolation framework (separate train).
4375                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4376                // internally; SPG behaves the same (effectively
4377                // READ COMMITTED at every level today).
4378                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4379                {
4380                    self.advance(); // TRANSACTION
4381                    let modes = self.parse_isolation_level_clauses()?;
4382                    return Ok(Statement::SetTransaction { modes });
4383                }
4384                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4385                // alias — same accept-as-no-op as SET NAMES.
4386                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4387                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4388                {
4389                    self.advance(); // CHARACTER
4390                    self.advance(); // SET
4391                    if matches!(
4392                        self.peek(),
4393                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4394                    ) {
4395                        self.advance();
4396                    }
4397                    return Ok(Statement::Empty);
4398                }
4399                // v7.39 (GUC) — PG spells the timezone GUC as two
4400                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4401                // where <value> is a string/ident or the LOCAL /
4402                // DEFAULT keyword (both mean "back to the default").
4403                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4404                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4405                {
4406                    self.advance(); // TIME
4407                    self.advance(); // ZONE
4408                    let value = match self.peek().clone() {
4409                        Token::Ident(s)
4410                            if s.eq_ignore_ascii_case("local")
4411                                || s.eq_ignore_ascii_case("default") =>
4412                        {
4413                            self.advance();
4414                            crate::ast::SetValue::Default
4415                        }
4416                        Token::Default => {
4417                            self.advance();
4418                            crate::ast::SetValue::Default
4419                        }
4420                        _ => self.parse_set_value()?,
4421                    };
4422                    return Ok(Statement::SetParameter {
4423                        name: "timezone".into(),
4424                        value,
4425                        local: set_local,
4426                    });
4427                }
4428                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4429                // MySQL USER-variable assignment: its own per-session
4430                // namespace, an arbitrary expression on the right, and `:=`
4431                // as a second spelling of `=`. It used to fall into the
4432                // session-PARAMETER list below, whose values are literals and
4433                // whose store nothing reads back under a `@` name — so the
4434                // assignment reported success and vanished.
4435                //
4436                // A `@@`-prefixed LHS is a real engine setting and keeps the
4437                // old path.
4438                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4439                    return self.parse_set_user_vars();
4440                }
4441                // v7.14.0 — multi-assignment form
4442                // `SET a = 1, b = 2, …`. Single-assignment is the
4443                // 1-element case. Each LHS may be a regular ident
4444                // or a SessionVar (`@VAR` / `@@VAR`).
4445                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4446                loop {
4447                    let lhs = match self.peek().clone() {
4448                        Token::SessionVar(s) => {
4449                            self.advance();
4450                            s
4451                        }
4452                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4453                        other => {
4454                            return Err(self.err(format!(
4455                                "expected parameter name after SET, got {other:?}"
4456                            )));
4457                        }
4458                    };
4459                    // Accept either `=` or the bare `TO` keyword.
4460                    match self.peek() {
4461                        Token::Eq => {
4462                            self.advance();
4463                        }
4464                        Token::To => {
4465                            self.advance();
4466                        }
4467                        other => {
4468                            return Err(self.err(format!(
4469                                "expected `=` or TO after SET {lhs}, got {other:?}"
4470                            )));
4471                        }
4472                    }
4473                    let mut value = self.parse_set_value()?;
4474                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4475                    // `, name TO` continues a MySQL-style multi-assign,
4476                    // anything else is a PG list VALUE
4477                    // (`SET search_path = myschema, public`) folded into
4478                    // one comma-joined string.
4479                    while matches!(self.peek(), Token::Comma) {
4480                        let is_assign = matches!(
4481                            self.tokens.get(self.pos + 1),
4482                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4483                        ) && matches!(
4484                            self.tokens.get(self.pos + 2),
4485                            Some(Token::Eq | Token::To)
4486                        );
4487                        if is_assign {
4488                            break;
4489                        }
4490                        self.advance(); // comma
4491                        let next = self.parse_set_value()?;
4492                        let joined = alloc::format!(
4493                            "{}, {}",
4494                            set_value_text(&value),
4495                            set_value_text(&next)
4496                        );
4497                        value = crate::ast::SetValue::String(joined);
4498                    }
4499                    pairs.push((lhs, value));
4500                    if matches!(self.peek(), Token::Comma) {
4501                        self.advance();
4502                        continue;
4503                    }
4504                    break;
4505                }
4506                if pairs.len() == 1 {
4507                    let (name, value) = pairs.into_iter().next().unwrap();
4508                    Ok(Statement::SetParameter {
4509                        name,
4510                        value,
4511                        local: set_local,
4512                    })
4513                } else {
4514                    Ok(Statement::SetParameterList(pairs))
4515                }
4516            }
4517            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4518            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4519                self.advance();
4520                match self.peek().clone() {
4521                    Token::All => {
4522                        self.advance();
4523                        Ok(Statement::ResetParameter(None))
4524                    }
4525                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4526                        self.advance();
4527                        Ok(Statement::ResetParameter(None))
4528                    }
4529                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4530                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4531                        self.advance();
4532                        Ok(Statement::SetRole(None))
4533                    }
4534                    // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4535                    // (pg_dump's return from the owner switch).
4536                    Token::Ident(s) | Token::QuotedIdent(s)
4537                        if s.eq_ignore_ascii_case("session")
4538                            && matches!(
4539                                self.tokens.get(self.pos + 1),
4540                                Some(Token::Ident(a) | Token::QuotedIdent(a))
4541                                    if a.eq_ignore_ascii_case("authorization")
4542                            ) =>
4543                    {
4544                        self.advance(); // SESSION
4545                        self.advance(); // AUTHORIZATION
4546                        Ok(Statement::SetRole(None))
4547                    }
4548                    _ => {
4549                        let name = self.parse_set_param_name()?;
4550                        Ok(Statement::ResetParameter(Some(name)))
4551                    }
4552                }
4553            }
4554            // v7.39 (round 218) — server-side cursors.
4555            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4556            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4557            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4558            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4559                self.advance();
4560                match self.peek().clone() {
4561                    Token::All => {
4562                        self.advance();
4563                        Ok(Statement::CloseCursor { name: None })
4564                    }
4565                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4566                        self.advance();
4567                        Ok(Statement::CloseCursor { name: None })
4568                    }
4569                    Token::Ident(n) | Token::QuotedIdent(n) => {
4570                        self.advance();
4571                        Ok(Statement::CloseCursor { name: Some(n) })
4572                    }
4573                    other => Err(self.err(format!(
4574                        "expected cursor name or ALL after CLOSE, got {other:?}"
4575                    ))),
4576                }
4577            }
4578            other => Err(self.err(format!(
4579                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4580                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4581            ))),
4582        }
4583    }
4584
4585    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4586    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4587    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4588    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4589    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4590        self.advance(); // DECLARE
4591        let name = match self.advance() {
4592            Token::Ident(n) | Token::QuotedIdent(n) => n,
4593            other => {
4594                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4595            }
4596        };
4597        let mut scroll: Option<bool> = None;
4598        loop {
4599            match self.peek() {
4600                Token::Ident(s)
4601                    if s.eq_ignore_ascii_case("binary")
4602                        || s.eq_ignore_ascii_case("insensitive")
4603                        || s.eq_ignore_ascii_case("asensitive") =>
4604                {
4605                    self.advance();
4606                }
4607                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4608                    self.advance();
4609                    scroll = Some(true);
4610                }
4611                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4612                {
4613                    self.advance(); // NO
4614                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4615                        return Err(self.err(format!(
4616                            "expected SCROLL after NO in DECLARE, got {:?}",
4617                            self.peek()
4618                        )));
4619                    }
4620                    self.advance();
4621                    scroll = Some(false);
4622                }
4623                _ => break,
4624            }
4625        }
4626        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4627            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4628        }
4629        self.advance();
4630        let mut hold = false;
4631        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4632            self.advance();
4633            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4634                return Err(self.err(format!(
4635                    "expected HOLD after WITH in DECLARE, got {:?}",
4636                    self.peek()
4637                )));
4638            }
4639            self.advance();
4640            hold = true;
4641        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4642            self.advance();
4643            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4644                return Err(self.err(format!(
4645                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4646                    self.peek()
4647                )));
4648            }
4649            self.advance();
4650        }
4651        if !matches!(self.peek(), Token::For) {
4652            return Err(self.err(format!(
4653                "expected FOR before the cursor query, got {:?}",
4654                self.peek()
4655            )));
4656        }
4657        self.advance();
4658        let query = self.parse_one_statement()?;
4659        Ok(Statement::DeclareCursor {
4660            name,
4661            scroll,
4662            hold,
4663            query: alloc::boxed::Box::new(query),
4664        })
4665    }
4666
4667    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4668    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4669    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4670    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4671        use crate::ast::CursorDirection as D;
4672        self.advance(); // FETCH / MOVE
4673        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4674            let neg = if matches!(this.peek(), Token::Minus) {
4675                this.advance();
4676                true
4677            } else {
4678                false
4679            };
4680            match this.advance() {
4681                Token::Integer(v) => Ok(if neg { -v } else { v }),
4682                other => Err(this.err(format!("expected count, got {other:?}"))),
4683            }
4684        };
4685        let direction = match self.peek().clone() {
4686            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4687                self.advance();
4688                D::Next
4689            }
4690            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4691                self.advance();
4692                D::Prior
4693            }
4694            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4695                self.advance();
4696                D::First
4697            }
4698            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4699                self.advance();
4700                D::Last
4701            }
4702            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4703                self.advance();
4704                D::Absolute(signed_count(self)?)
4705            }
4706            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4707                self.advance();
4708                D::Relative(signed_count(self)?)
4709            }
4710            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4711                self.advance();
4712                match self.peek().clone() {
4713                    Token::All => {
4714                        self.advance();
4715                        D::All
4716                    }
4717                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4718                        self.advance();
4719                        D::All
4720                    }
4721                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4722                    _ => D::Next, // bare FORWARD = FORWARD 1
4723                }
4724            }
4725            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4726                self.advance();
4727                match self.peek().clone() {
4728                    Token::All => {
4729                        self.advance();
4730                        D::BackwardAll
4731                    }
4732                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4733                        self.advance();
4734                        D::BackwardAll
4735                    }
4736                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4737                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4738                }
4739            }
4740            Token::All => {
4741                self.advance();
4742                D::All
4743            }
4744            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4745                self.advance();
4746                D::All
4747            }
4748            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4749            // Bare `FETCH <name>` — direction defaults to NEXT.
4750            _ => D::Next,
4751        };
4752        // Optional FROM / IN.
4753        if matches!(self.peek(), Token::From)
4754            || matches!(self.peek(), Token::In)
4755            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4756        {
4757            self.advance();
4758        }
4759        let name = match self.advance() {
4760            Token::Ident(n) | Token::QuotedIdent(n) => n,
4761            other => {
4762                return Err(self.err(format!("expected cursor name, got {other:?}")));
4763            }
4764        };
4765        Ok(if is_move {
4766            Statement::MoveCursor { name, direction }
4767        } else {
4768            Statement::FetchCursor { name, direction }
4769        })
4770    }
4771
4772    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4773    /// [(kind, …)] ON <col>, … FROM <table>`.
4774    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4775        self.advance(); // STATISTICS
4776        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4777        let mut if_not_exists = false;
4778        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4779            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4780        {
4781            self.advance();
4782            self.advance();
4783            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4784                self.advance();
4785                if_not_exists = true;
4786            }
4787        }
4788        let name = self.expect_ident_like()?;
4789        let mut kinds = Vec::new();
4790        if matches!(self.peek(), Token::LParen) {
4791            self.advance();
4792            loop {
4793                let k = self.expect_ident_like()?;
4794                // PG stores the single letters; accept the spelled-out
4795                // names the SQL uses and record what PG records.
4796                kinds.push(match k.to_ascii_lowercase().as_str() {
4797                    "ndistinct" => String::from("d"),
4798                    "dependencies" => String::from("f"),
4799                    "mcv" => String::from("m"),
4800                    other => {
4801                        return Err(
4802                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4803                        );
4804                    }
4805                });
4806                match self.advance() {
4807                    Token::Comma => {}
4808                    Token::RParen => break,
4809                    other => {
4810                        return Err(self.err(alloc::format!(
4811                            "expected ',' or ')' in statistics kind list, got {other:?}"
4812                        )));
4813                    }
4814                }
4815            }
4816        }
4817        if !matches!(self.peek(), Token::On) {
4818            return Err(self.err(alloc::format!(
4819                "expected ON in CREATE STATISTICS, got {:?}",
4820                self.peek()
4821            )));
4822        }
4823        self.advance();
4824        let mut columns = Vec::new();
4825        loop {
4826            columns.push(self.expect_ident_like()?);
4827            if matches!(self.peek(), Token::Comma) {
4828                self.advance();
4829            } else {
4830                break;
4831            }
4832        }
4833        if !matches!(self.peek(), Token::From) {
4834            return Err(self.err(alloc::format!(
4835                "expected FROM in CREATE STATISTICS, got {:?}",
4836                self.peek()
4837            )));
4838        }
4839        self.advance();
4840        let table = self.expect_ident_like()?;
4841        Ok(Statement::CreateStatistics {
4842            name,
4843            if_not_exists,
4844            kinds,
4845            columns,
4846            table,
4847        })
4848    }
4849
4850    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4851    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4852    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4853    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4854    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4855    /// forward call.
4856    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4857        self.advance(); // TABLE
4858        let if_exists = self.consume_if_exists();
4859        let mut names: Vec<String> = Vec::new();
4860        loop {
4861            names.push(self.expect_ident_like()?);
4862            if matches!(self.peek(), Token::Comma) {
4863                self.advance();
4864                continue;
4865            }
4866            break;
4867        }
4868        if matches!(
4869            self.peek(),
4870            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4871                || s.eq_ignore_ascii_case("restrict")
4872        ) {
4873            self.advance();
4874        }
4875        Ok(Statement::DropTable { names, if_exists })
4876    }
4877
4878    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4879        self.advance(); // STATISTICS
4880        let mut if_exists = false;
4881        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4882            && matches!(self.tokens.get(self.pos + 1),
4883                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4884        {
4885            self.advance();
4886            self.advance();
4887            if_exists = true;
4888        }
4889        let name = self.expect_ident_like()?;
4890        Ok(Statement::DropStatistics { name, if_exists })
4891    }
4892
4893    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4894        debug_assert!(matches!(self.peek(), Token::Create));
4895        self.advance();
4896        match self.peek() {
4897            Token::Table => self.parse_create_table_stmt_after_create(),
4898            Token::Index => self.parse_create_index_stmt_after_create(false),
4899            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4900            // object now. It used to be consumed by the CREATE-noise
4901            // arm, so a pg_dump that declares extended statistics
4902            // restored silently without them and reflection showed
4903            // nothing.
4904            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4905                self.parse_create_statistics_after_create()
4906            }
4907            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4908            // The `UNIQUE` modifier turns a partial index into a
4909            // partial-uniqueness invariant (only rows matching the
4910            // WHERE predicate are checked for duplicates). mailrs
4911            // K1 (3 hits: email_templates default, calendar_events
4912            // master, calendar_events instance).
4913            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4914                self.advance();
4915                if !matches!(self.peek(), Token::Index) {
4916                    return Err(self.err(alloc::format!(
4917                        "expected INDEX after CREATE UNIQUE, got {:?}",
4918                        self.peek()
4919                    )));
4920                }
4921                self.parse_create_index_stmt_after_create(true)
4922            }
4923            Token::Publication => {
4924                self.advance();
4925                self.parse_create_publication_after_keyword()
4926            }
4927            Token::Subscription => {
4928                self.advance();
4929                self.parse_create_subscription_after_keyword()
4930            }
4931            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4932            // USER isn't a reserved keyword — we look for the bare
4933            // identifier so the lexer doesn't have to grow a token.
4934            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4935                self.advance();
4936                self.parse_create_user_after_keyword(true)
4937            }
4938            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4939            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4940            // the default of the LOGIN attribute.
4941            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4942                self.advance();
4943                self.parse_create_user_after_keyword(false)
4944            }
4945            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4946            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4947                self.advance();
4948                self.parse_create_policy_after_keyword()
4949            }
4950            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4951            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4952            // no-op. mailrs follow-up F3.
4953            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4954                self.advance();
4955                self.parse_create_extension_after_keyword()
4956            }
4957            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4958            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4959            // optional; absorb it here and forward to the
4960            // per-kind parsers with the flag. OR is a reserved
4961            // keyword token.
4962            Token::Or => {
4963                self.advance();
4964                let next = self.peek();
4965                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4966                    return Err(self.err(alloc::format!(
4967                        "expected REPLACE after CREATE OR, got {next:?}"
4968                    )));
4969                };
4970                if !s2.eq_ignore_ascii_case("replace") {
4971                    return Err(self.err(alloc::format!(
4972                        "expected REPLACE after CREATE OR, got {s2:?}"
4973                    )));
4974                }
4975                self.advance();
4976                self.parse_create_function_or_trigger_after_or_replace(true)
4977            }
4978            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
4979                self.advance();
4980                self.parse_create_function_after_keyword(false)
4981            }
4982            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
4983                self.advance();
4984                self.parse_create_trigger_after_keyword(false)
4985            }
4986            // v7.39 (round 139) — CREATE RULE …
4987            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
4988                self.advance();
4989                self.parse_create_rule_after_keyword(false)
4990            }
4991            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
4992            // trigger is a row-level AFTER trigger that additionally carries
4993            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
4994            // path already tolerates and skips those clauses, so consuming the
4995            // CONSTRAINT keyword and reusing it makes the statement parse and the
4996            // trigger fire. (The deferral timing itself is not yet honoured —
4997            // SPG fires it as a plain AFTER trigger, which is correct behaviour
4998            // for every non-deferred use.)
4999            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5000                self.advance();
5001                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
5002                    if t.eq_ignore_ascii_case("trigger"))
5003                {
5004                    return Err(self.err(alloc::format!(
5005                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
5006                        self.peek()
5007                    )));
5008                }
5009                self.advance();
5010                self.parse_create_trigger_after_keyword(false)
5011            }
5012            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
5013            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
5014                self.advance();
5015                self.parse_create_sequence_after_keyword(false)
5016            }
5017            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
5018            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
5019                self.advance();
5020                self.parse_create_view_after_keyword(false, false, false)
5021            }
5022            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
5023            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
5024            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
5025            // appear (in any order) between `CREATE` and `VIEW` in
5026            // every mysqldump-emitted view. Pre-2.6 the parser
5027            // rejected the prefix and the customer's whole view
5028            // backup failed on the first view. The hints are pure
5029            // planner / permission metadata; SPG's view-rewrite
5030            // path is semantically equivalent for all three
5031            // algorithms in v7.17 (TEMPTABLE differs only in
5032            // perf for huge views — out of v7.17 scope), and
5033            // DEFINER / SQL SECURITY are pure single-user
5034            // permissioning that SPG ignores by design.
5035            Token::Ident(s) | Token::QuotedIdent(s)
5036                if s.eq_ignore_ascii_case("algorithm")
5037                    || s.eq_ignore_ascii_case("definer")
5038                    || s.eq_ignore_ascii_case("sql") =>
5039            {
5040                self.consume_mysql_view_prefix()?;
5041                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
5042                // (in any order, in any combination), the next
5043                // keyword must be VIEW. mysqldump never emits these
5044                // prefixes on non-view statements.
5045                let next = self.peek().clone();
5046                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
5047                    if s2.eq_ignore_ascii_case("view"))
5048                {
5049                    self.advance();
5050                    self.parse_create_view_after_keyword(false, false, false)
5051                } else {
5052                    Err(self.err(alloc::format!(
5053                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
5054                    )))
5055                }
5056            }
5057            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
5058            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
5059                self.advance();
5060                self.parse_create_type_after_keyword()
5061            }
5062            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
5063            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
5064            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
5065                self.advance();
5066                self.parse_create_domain_after_keyword()
5067            }
5068            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
5069            // name [AUTHORIZATION user]. Real catalog registry
5070            // (was silent-no-op'd pre-v7.17).
5071            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
5072                self.advance();
5073                let if_not_exists = self.parse_if_not_exists();
5074                let name = self.expect_ident_like()?;
5075                // Optional `AUTHORIZATION <user>` trailer — accepted,
5076                // ignored (single-user catalog).
5077                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5078                    if s.eq_ignore_ascii_case("authorization"))
5079                {
5080                    self.advance();
5081                    let _ = self.expect_ident_like()?;
5082                }
5083                Ok(Statement::CreateSchema { name, if_not_exists })
5084            }
5085            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5086            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5087                self.advance();
5088                let next = self.peek().clone();
5089                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5090                {
5091                    self.advance();
5092                    self.parse_create_materialized_view_after_keyword()
5093                } else {
5094                    Err(self.err(alloc::format!(
5095                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5096                    )))
5097                }
5098            }
5099            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5100            // no-op below), an UNLOGGED table is a real, fully-usable table in
5101            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5102            // durability optimisation is a follow-up), so a dump / app that
5103            // declares UNLOGGED tables works instead of failing to parse.
5104            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5105                self.advance(); // UNLOGGED
5106                if matches!(self.peek(), Token::Table) {
5107                    self.parse_create_table_stmt_after_create()
5108                } else {
5109                    Err(self.err(format!(
5110                        "expected TABLE after CREATE UNLOGGED, got {:?}",
5111                        self.peek()
5112                    )))
5113                }
5114            }
5115            Token::Ident(s) | Token::QuotedIdent(s)
5116                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5117            {
5118                self.advance();
5119                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5120                let next = self.peek().clone();
5121                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5122                {
5123                    self.advance();
5124                    self.parse_create_sequence_after_keyword(true)
5125                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5126                {
5127                    self.advance();
5128                    self.parse_create_view_after_keyword(false, false, true)
5129                } else {
5130                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5131                    // consumed and answered OK while creating nothing, so
5132                    // every statement that touched the table afterwards failed
5133                    // with "table not found" — the DDL itself lied. It is a
5134                    // real CREATE TABLE now, marked temporary so the executor
5135                    // puts it in the session's own namespace. An optional
5136                    // TABLE keyword may or may not be present (`CREATE TEMP t`
5137                    // is not legal, but the keyword is consumed by the
5138                    // CREATE TABLE parser itself).
5139                    let stmt = self.parse_create_table_stmt_after_create()?;
5140                    match stmt {
5141                        Statement::CreateTable(mut c) => {
5142                            c.temporary = true;
5143                            Ok(Statement::CreateTable(c))
5144                        }
5145                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5146                        // CTAS node, which needs the same session namespace.
5147                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5148                            m.temporary = true;
5149                            Ok(Statement::CreateMaterializedView(m))
5150                        }
5151                        other => Ok(other),
5152                    }
5153                }
5154            }
5155            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5156            // BEGIN <body> END`. The body may reference `@var`
5157            // session variables, SET statements, internal `;`
5158            // terminators, etc. SPG has no procedure runtime, so
5159            // consume the whole `CREATE PROCEDURE … END` block as
5160            // a no-op so mysqldump scripts that include stored
5161            // routines load through. The matching-END consumer
5162            // tracks BEGIN/END nesting depth to handle nested
5163            // BEGIN blocks correctly.
5164            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5165                self.consume_mysql_routine_body();
5166                Ok(Statement::Empty)
5167            }
5168            // v7.14.0 — pg_dump / mysqldump emit
5169            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5170            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5171            // SPG is single-schema / single-database; these have
5172            // no behavioural effect, so consume + return Empty.
5173            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5174            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5175            // moved up to real parser branches. DATABASE / ROLE /
5176            // POLICY / OPERATOR stay no-op forever
5177            // (single-database, hardcoded roles).
5178            Token::Ident(s) | Token::QuotedIdent(s)
5179                if matches!(
5180                    s.to_ascii_lowercase().as_str(),
5181                    "database"
5182                        | "role"
5183                        | "operator"
5184                        | "cast"
5185                        | "aggregate"
5186                        | "language"
5187                        | "collation"
5188                        | "conversion"
5189                        // v7.17.0 Phase 8 (audit N6) — rarely-
5190                        // emitted pg_dump shapes that should
5191                        // load through without a parser error.
5192                        // SPG has no planner statistics catalog,
5193                        // no event-trigger hooks, no foreign-
5194                        // data-wrapper infrastructure; consume
5195                        // + return Empty.
5196                        | "statistics"
5197                        | "event"
5198                        // v7.37.17 (17.6 siblings) — additional CREATE
5199                        // targets pg_dump / operator install scripts
5200                        // may emit that SPG has no matching machinery
5201                        // for. Consume + Empty-return.
5202                        | "text"
5203                        | "tablespace"
5204                        | "access"
5205                        | "large"
5206                ) =>
5207            {
5208                // DATABASE is the one member of this list PG refuses
5209                // inside a transaction block; the rest (ROLE, CAST,
5210                // TABLESPACE, …) it runs there quite happily, so only
5211                // this one is named. Still a no-op otherwise — SPG is
5212                // single-database.
5213                let is_database = s.eq_ignore_ascii_case("database");
5214                // The name is the first token after DATABASE, past an
5215                // `IF NOT EXISTS`.
5216                let name = if is_database {
5217                    self.scan_database_name()
5218                } else {
5219                    None
5220                };
5221                let collation = if is_database {
5222                    self.scan_database_collation_until_boundary()
5223                } else {
5224                    self.consume_until_statement_boundary();
5225                    None
5226                };
5227                if is_database {
5228                    return Ok(Statement::NoOpPreventedInTransaction {
5229                        what: String::from("CREATE DATABASE"),
5230                        collation,
5231                        name,
5232                    });
5233                }
5234                Ok(Statement::Empty)
5235            }
5236            // v7.39 (round 706) — the foreign-data family leaves the silent
5237            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5238            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5239            // FDW machinery), but the ENGINE now warns, so a restore log
5240            // says what will not function instead of reporting success.
5241            Token::Ident(s) | Token::QuotedIdent(s)
5242                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5243            {
5244                self.consume_until_statement_boundary();
5245                Ok(Statement::ValidateOnly {
5246                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5247                    names: Vec::new(),
5248                })
5249            }
5250            other => Err(self.err(format!(
5251                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5252            ))),
5253        }
5254    }
5255
5256    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5257    /// keyword decides whether we parse a function or trigger
5258    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5259    /// PROCEDURE) — those land in later releases.
5260    fn parse_create_function_or_trigger_after_or_replace(
5261        &mut self,
5262        or_replace: bool,
5263    ) -> Result<Statement, ParseError> {
5264        let tok = self.peek();
5265        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5266            return Err(self.err(alloc::format!(
5267                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5268            )));
5269        };
5270        if s.eq_ignore_ascii_case("function") {
5271            self.advance();
5272            self.parse_create_function_after_keyword(or_replace)
5273        } else if s.eq_ignore_ascii_case("trigger") {
5274            self.advance();
5275            self.parse_create_trigger_after_keyword(or_replace)
5276        } else if s.eq_ignore_ascii_case("rule") {
5277            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5278            self.advance();
5279            self.parse_create_rule_after_keyword(or_replace)
5280        } else if s.eq_ignore_ascii_case("view") {
5281            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5282            self.advance();
5283            self.parse_create_view_after_keyword(or_replace, false, false)
5284        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5285            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5286            self.advance();
5287            let nxt = self.peek().clone();
5288            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5289            {
5290                self.advance();
5291                self.parse_create_view_after_keyword(or_replace, false, true)
5292            } else {
5293                Err(self.err(alloc::format!(
5294                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5295                )))
5296            }
5297        } else {
5298            Err(self.err(alloc::format!(
5299                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5300            )))
5301        }
5302    }
5303
5304    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5305    /// SPG doesn't have a registry; pgvector / similar are
5306    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5307    /// the syntax lets dual-target schemas keep the line.
5308    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5309        // Optional `IF NOT EXISTS`.
5310        self.consume_if_not_exists();
5311        let name = self.expect_ident_like()?;
5312        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5313        // CASCADE / FROM '<v>' clauses; we don't model them.
5314        loop {
5315            match self.peek() {
5316                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5317                    self.advance();
5318                    continue;
5319                }
5320                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5321                    self.advance();
5322                    let _ = self.expect_ident_like()?;
5323                    continue;
5324                }
5325                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5326                    self.advance();
5327                    // String or ident literal.
5328                    let _ = self.advance();
5329                    continue;
5330                }
5331                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5332                    self.advance();
5333                    let _ = self.advance();
5334                    continue;
5335                }
5336                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5337                    self.advance();
5338                    continue;
5339                }
5340                _ => break,
5341            }
5342        }
5343        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5344        // nosuch` reported success and `pg_extension` then did not list it,
5345        // which is the accept-and-do-nothing shape F31 exists to find.
5346        Ok(Statement::ValidateOnly {
5347            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5348            names: alloc::vec![name],
5349        })
5350    }
5351
5352    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5353    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5354    /// already been consumed by the caller. Grammar accepted:
5355    ///
5356    ///   name `(` arg-list `)`
5357    ///   `RETURNS` return-type
5358    ///   [ `LANGUAGE` ident ]
5359    ///   `AS` $$ body $$
5360    ///   [ `LANGUAGE` ident ]
5361    ///
5362    /// Either `LANGUAGE` position is allowed; PG accepts both.
5363    fn parse_create_function_after_keyword(
5364        &mut self,
5365        or_replace: bool,
5366    ) -> Result<Statement, ParseError> {
5367        let name = self.expect_ident_like()?;
5368        // Argument list. v7.12.4 commonly sees the empty `()`
5369        // (trigger functions); typed args parse and round-trip
5370        // but the executor only invokes nullary functions.
5371        if !matches!(self.peek(), Token::LParen) {
5372            return Err(self.err(alloc::format!(
5373                "expected '(' after function name {name:?}, got {:?}",
5374                self.peek()
5375            )));
5376        }
5377        self.advance();
5378        let args = self.parse_function_arg_list()?;
5379        // RETURNS clause.
5380        let tok = self.peek();
5381        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5382            return Err(self.err(alloc::format!(
5383                "expected RETURNS after function arg list, got {tok:?}"
5384            )));
5385        };
5386        if !s.eq_ignore_ascii_case("returns") {
5387            return Err(self.err(alloc::format!(
5388                "expected RETURNS after function arg list, got {s:?}"
5389            )));
5390        }
5391        self.advance();
5392        let returns = self.parse_function_return()?;
5393        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5394        // re-check after the body too).
5395        let mut language: Option<String> = self.parse_optional_language()?;
5396        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5397        // either side of the body and in any order, interleaved with
5398        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5399        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5400        // PG's own pg_dump output did not restore.
5401        let mut attrs = FunctionAttrs::default();
5402        loop {
5403            let before = self.pos;
5404            self.parse_function_attrs_into(&mut attrs)?;
5405            if language.is_none() {
5406                language = self.parse_optional_language()?;
5407            }
5408            if self.pos == before {
5409                break;
5410            }
5411        }
5412        // `AS` followed by a $$-quoted body (lexer already
5413        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5414        // Token::String). AS is a reserved keyword (Token::As).
5415        if !matches!(self.peek(), Token::As) {
5416            return Err(self.err(alloc::format!(
5417                "expected AS before function body, got {:?}",
5418                self.peek()
5419            )));
5420        }
5421        self.advance();
5422        let body_text = match self.peek() {
5423            Token::String(s) => {
5424                let body = s.clone();
5425                self.advance();
5426                body
5427            }
5428            other => {
5429                return Err(self.err(alloc::format!(
5430                    "expected $$-quoted function body after AS, got {other:?}"
5431                )));
5432            }
5433        };
5434        // Trailing clauses — PG's other accepted position for both the
5435        // LANGUAGE and the attributes.
5436        loop {
5437            let before = self.pos;
5438            self.parse_function_attrs_into(&mut attrs)?;
5439            if language.is_none() {
5440                language = self.parse_optional_language()?;
5441            }
5442            if self.pos == before {
5443                break;
5444            }
5445        }
5446        let language = language.unwrap_or_else(|| String::from("sql"));
5447        // PL/pgSQL bodies get structure-parsed. Other languages
5448        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5449        // recognise) round-trip as Raw text — the executor errors
5450        // when invoked with a clear unsupported message.
5451        let body = if language.eq_ignore_ascii_case("plpgsql") {
5452            match parse_plpgsql_body(&body_text) {
5453                Ok(block) => FunctionBody::PlPgSql(block),
5454                // Best-effort: if the body parser doesn't yet
5455                // support a construct used inside, fall back to
5456                // raw — keeps `CREATE FUNCTION` itself working
5457                // (catalogue accepts), executor errors on
5458                // invocation only.
5459                Err(_) => FunctionBody::Raw(body_text),
5460            }
5461        } else {
5462            FunctionBody::Raw(body_text)
5463        };
5464        Ok(Statement::CreateFunction(CreateFunctionStatement {
5465            name,
5466            or_replace,
5467            args,
5468            returns,
5469            language,
5470            body,
5471            attrs,
5472        }))
5473    }
5474
5475    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5476    /// attribute clauses into `attrs`, stopping at the first token that
5477    /// is not one. Measured against PG 18.4, which accepts them in any
5478    /// order and on either side of the body.
5479    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5480        loop {
5481            let word = match self.peek() {
5482                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5483                // NOT LEAKPROOF — NOT is a reserved keyword token.
5484                Token::Not
5485                    if matches!(
5486                        self.tokens.get(self.pos + 1),
5487                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5488                    ) =>
5489                {
5490                    self.advance();
5491                    self.advance();
5492                    attrs.leakproof = false;
5493                    continue;
5494                }
5495                _ => return Ok(()),
5496            };
5497            match word.as_str() {
5498                "immutable" => {
5499                    self.advance();
5500                    attrs.volatility = FunctionVolatility::Immutable;
5501                }
5502                "stable" => {
5503                    self.advance();
5504                    attrs.volatility = FunctionVolatility::Stable;
5505                }
5506                "volatile" => {
5507                    self.advance();
5508                    attrs.volatility = FunctionVolatility::Volatile;
5509                }
5510                "strict" => {
5511                    self.advance();
5512                    attrs.strict = true;
5513                }
5514                "leakproof" => {
5515                    self.advance();
5516                    attrs.leakproof = true;
5517                }
5518                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5519                // spelled-out forms of STRICT and its opposite.
5520                "returns" | "called" => {
5521                    let strict = word == "returns";
5522                    let mut probe = self.pos + 1;
5523                    if strict {
5524                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5525                        // is not ours.
5526                        match self.tokens.get(probe) {
5527                            Some(Token::Null) => probe += 1,
5528                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5529                            _ => return Ok(()),
5530                        }
5531                    }
5532                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5533                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5534                    if !ok {
5535                        return Ok(());
5536                    }
5537                    probe += 1;
5538                    match self.tokens.get(probe) {
5539                        Some(Token::Null) => probe += 1,
5540                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5541                        _ => return Ok(()),
5542                    }
5543                    match self.tokens.get(probe) {
5544                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5545                        _ => return Ok(()),
5546                    }
5547                    self.pos = probe;
5548                    attrs.strict = strict;
5549                }
5550                "security" | "external" => {
5551                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5552                    let mut probe = self.pos + 1;
5553                    if word == "external" {
5554                        match self.tokens.get(probe) {
5555                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5556                                probe += 1;
5557                            }
5558                            _ => return Ok(()),
5559                        }
5560                    }
5561                    let definer = match self.tokens.get(probe) {
5562                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5563                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5564                        _ => return Ok(()),
5565                    };
5566                    self.pos = probe + 1;
5567                    attrs.security_definer = definer;
5568                }
5569                "parallel" => {
5570                    let level = match self.tokens.get(self.pos + 1) {
5571                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5572                            FunctionParallel::Safe
5573                        }
5574                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5575                            FunctionParallel::Restricted
5576                        }
5577                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5578                            FunctionParallel::Unsafe
5579                        }
5580                        _ => return Ok(()),
5581                    };
5582                    self.pos += 2;
5583                    attrs.parallel = level;
5584                }
5585                "cost" | "rows" => {
5586                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5587                        return Ok(());
5588                    };
5589                    self.pos += 2;
5590                    if word == "cost" {
5591                        attrs.cost = Some(n);
5592                    } else {
5593                        attrs.rows = Some(n);
5594                    }
5595                }
5596                _ => return Ok(()),
5597            }
5598        }
5599    }
5600
5601    /// The numeric literal at `idx`, if there is one.
5602    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5603        match self.tokens.get(idx)? {
5604            Token::Integer(n) => Some(*n as f64),
5605            Token::Float(f) => Some(*f),
5606            Token::Numeric(t) => t.parse::<f64>().ok(),
5607            _ => None,
5608        }
5609    }
5610
5611    /// Closing `)`-terminated argument list. v7.12.4 commonly
5612    /// sees the empty `()`; typed args round-trip but the
5613    /// executor (yet) doesn't invoke them.
5614    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5615    /// it away, which is what PG does with one on a function parameter.
5616    fn skip_type_modifier(&mut self) {
5617        if !matches!(self.peek(), Token::LParen) {
5618            return;
5619        }
5620        // Only a numeric modifier — anything else is not one, and eating
5621        // it would swallow real grammar.
5622        let mut i = self.pos + 1;
5623        let mut seen_number = false;
5624        loop {
5625            match self.tokens.get(i) {
5626                Some(Token::Integer(_)) => seen_number = true,
5627                Some(Token::Comma) => {}
5628                Some(Token::RParen) => break,
5629                _ => return,
5630            }
5631            i += 1;
5632        }
5633        if !seen_number {
5634            return;
5635        }
5636        while self.pos <= i {
5637            self.advance();
5638        }
5639    }
5640
5641    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5642        let mut args: Vec<FunctionArg> = Vec::new();
5643        if matches!(self.peek(), Token::RParen) {
5644            self.advance();
5645            return Ok(args);
5646        }
5647        loop {
5648            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5649            // a reserved token; OUT / INOUT are bare idents.
5650            let mode = if matches!(self.peek(), Token::In) {
5651                self.advance();
5652                FunctionArgMode::In
5653            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5654            {
5655                self.advance();
5656                FunctionArgMode::Out
5657            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5658            {
5659                self.advance();
5660                FunctionArgMode::InOut
5661            } else {
5662                FunctionArgMode::In
5663            };
5664            // Optional name. The next token is either a name
5665            // (followed by a type ident) or the type itself.
5666            // Disambiguate by peeking ahead: if the token after
5667            // the next ident is also an ident, we treat the
5668            // first as the name.
5669            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5670            // the comma or paren, then decide. Reading at most two of
5671            // them could not spell `x double precision` at all, and
5672            // silently mis-read the bare `double precision` as a
5673            // parameter named "double" — which is what made the same
5674            // signature key two different ways.
5675            let (name, ty_token) = {
5676                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5677                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5678                    words.push(self.expect_ident_like()?);
5679                }
5680                // v7.39 (round 344) — a length / precision modifier on the
5681                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5682                // accepts it and DROPS it — `pg_get_function_arguments`
5683                // reports plain `character varying` / `numeric`, measured on
5684                // 18.4 — but SPG raised `syntax error at or near "("`,
5685                // because the modifier's parens were never consumed.
5686                self.skip_type_modifier();
5687                // r1049 — `f(v bigint[])`. The array suffix parsed in
5688                // the column position, the cast position and (r1038)
5689                // the RETURNS position, but not here: the fifth
5690                // member of the same family, reported by sentori as
5691                // presumably the same code. It is now.
5692                let array_suffix = self.consume_array_suffix();
5693                let whole = words.join(" ");
5694                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5695                {
5696                    (Some(words[0].clone()), words[1..].join(" "))
5697                } else {
5698                    (None, whole)
5699                };
5700                ty_token.push_str(&array_suffix);
5701                (name, ty_token)
5702            };
5703            // Type — try to map to ColumnTypeName, else Raw.
5704            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5705                Some(t) => FunctionArgType::Typed(t),
5706                None => FunctionArgType::Raw(ty_token),
5707            };
5708            args.push(FunctionArg { mode, name, ty });
5709            match self.peek() {
5710                Token::Comma => {
5711                    self.advance();
5712                    continue;
5713                }
5714                Token::RParen => {
5715                    self.advance();
5716                    return Ok(args);
5717                }
5718                other => {
5719                    return Err(self.err(alloc::format!(
5720                        "expected , or ) in function arg list, got {other:?}"
5721                    )));
5722                }
5723            }
5724        }
5725    }
5726
5727    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5728        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5729        // function whose row shape is named inline.
5730        if matches!(self.peek(), Token::Table)
5731            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5732        {
5733            self.advance(); // TABLE
5734            self.advance(); // (
5735            let mut cols: Vec<String> = Vec::new();
5736            loop {
5737                let cname = self.expect_ident_like()?;
5738                let mut ty: Vec<String> = Vec::new();
5739                loop {
5740                    match self.peek() {
5741                        Token::Comma | Token::RParen | Token::Eof => break,
5742                        _ => {}
5743                    }
5744                    match self.advance() {
5745                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5746                        other => {
5747                            if let Some(w) = unreserved_keyword_text(&other) {
5748                                ty.push(w);
5749                            }
5750                        }
5751                    }
5752                }
5753                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5754                if matches!(self.peek(), Token::Comma) {
5755                    self.advance();
5756                } else {
5757                    break;
5758                }
5759            }
5760            if matches!(self.peek(), Token::RParen) {
5761                self.advance();
5762            }
5763            return Ok(FunctionReturn::Other(alloc::format!(
5764                "TABLE({})",
5765                cols.join(", ")
5766            )));
5767        }
5768        let ident = self.expect_ident_like()?;
5769        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5770        if ident.eq_ignore_ascii_case("setof") {
5771            let inner = self.expect_ident_like()?;
5772            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5773            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5774        }
5775        if ident.eq_ignore_ascii_case("trigger") {
5776            return Ok(FunctionReturn::Trigger);
5777        }
5778        if ident.eq_ignore_ascii_case("void") {
5779            return Ok(FunctionReturn::Void);
5780        }
5781        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5782        // RETURN position did not, so the `[` was a syntax error and the
5783        // whole migration stopped. sentori worked around it by returning
5784        // zero-padded text.
5785        let suffix = self.consume_array_suffix();
5786        if !suffix.is_empty() {
5787            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5788        }
5789        match map_type_ident_to_column_type_name(&ident) {
5790            Some(t) => Ok(FunctionReturn::Type(t)),
5791            None => Ok(FunctionReturn::Other(ident)),
5792        }
5793    }
5794
5795    /// Consume any `[]` / `[N]` array markers after a type name and give
5796    /// back their text. Empty when there are none.
5797    fn consume_array_suffix(&mut self) -> String {
5798        let mut out = String::new();
5799        while matches!(self.peek(), Token::LBracket) {
5800            self.advance();
5801            // `[N]` is accepted and, as in PG, the length is not enforced.
5802            if let Token::Integer(n) = self.peek().clone() {
5803                self.advance();
5804                out.push_str(&alloc::format!("[{n}]"));
5805            } else {
5806                out.push_str("[]");
5807            }
5808            if matches!(self.peek(), Token::RBracket) {
5809                self.advance();
5810            }
5811        }
5812        out
5813    }
5814
5815    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5816        match self.peek() {
5817            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5818                self.advance();
5819                let lang = self.expect_ident_like()?;
5820                Ok(Some(lang.to_ascii_lowercase()))
5821            }
5822            _ => Ok(None),
5823        }
5824    }
5825
5826    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5827    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5828    /// (expr)]*`. The `DOMAIN` keyword has already been
5829    /// consumed. PG allows the trailing constraints in any
5830    /// order; we approximate with a small loop.
5831    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5832        let name = self.expect_ident_like()?;
5833        // Optional `AS`.
5834        if matches!(self.peek(), Token::As) {
5835            self.advance();
5836        }
5837        // v7.39 (round 259) — keep the raw type NAME when the base is not
5838        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5839        // parent domain.
5840        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5841            self.parse_type_with_implied_flags()?;
5842        let mut default: Option<Expr> = None;
5843        let mut not_null = false;
5844        let mut checks: Vec<Expr> = Vec::new();
5845        loop {
5846            match self.peek() {
5847                Token::Default => {
5848                    if default.is_some() {
5849                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5850                    }
5851                    self.advance();
5852                    default = Some(self.parse_expr(0)?);
5853                }
5854                Token::Not => {
5855                    self.advance();
5856                    if !matches!(self.peek(), Token::Null) {
5857                        return Err(self.err(alloc::format!(
5858                            "expected NULL after NOT in DOMAIN, got {:?}",
5859                            self.peek()
5860                        )));
5861                    }
5862                    self.advance();
5863                    not_null = true;
5864                }
5865                Token::Null => {
5866                    self.advance();
5867                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5868                    // is the default-nullable marker (PG accepts it),
5869                    // but AFTER a NOT NULL it is a conflict PG refuses
5870                    // (`conflicting NULL/NOT NULL constraints`,
5871                    // PG18-measured); the old arm no-opped both ways.
5872                    if not_null {
5873                        return Err(self.err(alloc::string::String::from(
5874                            "conflicting NULL/NOT NULL constraints",
5875                        )));
5876                    }
5877                }
5878                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5879                    self.advance();
5880                    if !matches!(self.peek(), Token::LParen) {
5881                        return Err(self.err(alloc::format!(
5882                            "expected '(' after CHECK in DOMAIN, got {:?}",
5883                            self.peek()
5884                        )));
5885                    }
5886                    self.advance();
5887                    let expr = self.parse_expr(0)?;
5888                    if !matches!(self.peek(), Token::RParen) {
5889                        return Err(self.err(alloc::format!(
5890                            "expected ')' after CHECK expr, got {:?}",
5891                            self.peek()
5892                        )));
5893                    }
5894                    self.advance();
5895                    checks.push(expr);
5896                }
5897                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5898                // prefix on the constraint; we drop the name and
5899                // recurse into the constraint parsing.
5900                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5901                    self.advance();
5902                    let _ = self.expect_ident_like()?;
5903                }
5904                _ => break,
5905            }
5906        }
5907        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5908            name,
5909            base_type,
5910            base_domain: base_user_ref,
5911            default,
5912            not_null,
5913            checks,
5914        }))
5915    }
5916
5917    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5918    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5919    /// consumed.
5920    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5921        let name = self.expect_ident_like()?;
5922        // Required `AS`.
5923        if !matches!(self.peek(), Token::As) {
5924            return Err(self.err(alloc::format!(
5925                "expected AS after CREATE TYPE {name:?}, got {:?}",
5926                self.peek()
5927            )));
5928        }
5929        self.advance();
5930        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5931        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5932        // on the next token: `(` = composite, ident `ENUM` = enum.
5933        if matches!(self.peek(), Token::LParen) {
5934            self.advance();
5935            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5936            let mut field_user_types: Vec<Option<String>> = Vec::new();
5937            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5938            // is legal PG (an attribute-less composite; measured — the old
5939            // e2e note claimed PG requires at least one attribute).
5940            if matches!(self.peek(), Token::RParen) {
5941                self.advance();
5942                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5943                    name,
5944                    kind: crate::ast::TypeKind::Composite {
5945                        fields,
5946                        field_user_types,
5947                    },
5948                }));
5949            }
5950            loop {
5951                let field_name = self.expect_ident_like()?;
5952                // v7.39 (round 264) — keep the raw type name when it is not
5953                // a builtin: that is how a NESTED composite field records
5954                // which composite it holds.
5955                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
5956                    self.parse_type_with_implied_flags()?;
5957                fields.push((field_name, field_type));
5958                field_user_types.push(field_user_ref);
5959                if matches!(self.peek(), Token::Comma) {
5960                    self.advance();
5961                    continue;
5962                }
5963                if matches!(self.peek(), Token::RParen) {
5964                    self.advance();
5965                    break;
5966                }
5967                return Err(self.err(alloc::format!(
5968                    "expected , or ) in composite field list, got {:?}",
5969                    self.peek()
5970                )));
5971            }
5972            if fields.is_empty() {
5973                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5974            }
5975            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5976                name,
5977                kind: crate::ast::TypeKind::Composite {
5978                    fields,
5979                    field_user_types,
5980                },
5981            }));
5982        }
5983        // Required `ENUM` ident.
5984        let kind_ident = match self.peek().clone() {
5985            Token::Ident(s) | Token::QuotedIdent(s) => s,
5986            other => {
5987                return Err(self.err(alloc::format!(
5988                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
5989                )));
5990            }
5991        };
5992        if !kind_ident.eq_ignore_ascii_case("enum") {
5993            return Err(self.err(alloc::format!(
5994                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
5995            )));
5996        }
5997        self.advance();
5998        if !matches!(self.peek(), Token::LParen) {
5999            return Err(self.err(alloc::format!(
6000                "expected '(' after ENUM, got {:?}",
6001                self.peek()
6002            )));
6003        }
6004        self.advance();
6005        let mut labels: Vec<String> = Vec::new();
6006        loop {
6007            match self.peek().clone() {
6008                Token::String(s) => {
6009                    self.advance();
6010                    labels.push(s);
6011                }
6012                other => {
6013                    return Err(
6014                        self.err(alloc::format!("expected enum label string, got {other:?}"))
6015                    );
6016                }
6017            }
6018            if matches!(self.peek(), Token::Comma) {
6019                self.advance();
6020                continue;
6021            }
6022            if matches!(self.peek(), Token::RParen) {
6023                self.advance();
6024                break;
6025            }
6026            return Err(self.err(alloc::format!(
6027                "expected , or ) in ENUM label list, got {:?}",
6028                self.peek()
6029            )));
6030        }
6031        if labels.is_empty() {
6032            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
6033        }
6034        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6035            name,
6036            kind: crate::ast::TypeKind::Enum { labels },
6037        }))
6038    }
6039
6040    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
6041    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
6042    /// The `CREATE MATERIALIZED VIEW` keywords have already been
6043    /// consumed.
6044    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
6045        let if_not_exists = self.parse_if_not_exists();
6046        let name = self.expect_ident_like()?;
6047        let mut columns: Vec<String> = Vec::new();
6048        if matches!(self.peek(), Token::LParen) {
6049            self.advance();
6050            loop {
6051                let c = self.expect_ident_like()?;
6052                columns.push(c);
6053                if matches!(self.peek(), Token::Comma) {
6054                    self.advance();
6055                    continue;
6056                }
6057                if matches!(self.peek(), Token::RParen) {
6058                    self.advance();
6059                    break;
6060                }
6061                return Err(self.err(alloc::format!(
6062                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
6063                    self.peek()
6064                )));
6065            }
6066        }
6067        if !matches!(self.peek(), Token::As) {
6068            return Err(self.err(alloc::format!(
6069                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
6070                self.peek()
6071            )));
6072        }
6073        self.advance();
6074        // v7.39 (round 151) — a WITH-headed body is legal (read-only
6075        // CTEs only; the engine rejects data-modifying ones with PG's
6076        // message). A trailing `WITH [NO] DATA` can't START the body,
6077        // so WITH here heads the query.
6078        let body = if self.peek_is_with_kw() {
6079            self.advance();
6080            self.parse_nested_with_select()?
6081        } else {
6082            let body_stmt = self.parse_select_stmt()?;
6083            let Statement::Select(body) = body_stmt else {
6084                return Err(self.err(alloc::format!(
6085                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6086                )));
6087            };
6088            body
6089        };
6090        // Optional trailing `WITH [NO] DATA`.
6091        let with_data = self.parse_optional_with_data(true)?;
6092        Ok(Statement::CreateMaterializedView(
6093            crate::ast::CreateMaterializedViewStatement {
6094                temporary: false,
6095                name,
6096                if_not_exists,
6097                columns,
6098                body,
6099                with_data,
6100                as_plain_table: false,
6101            },
6102        ))
6103    }
6104
6105    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6106    /// `default_when_absent` is what to return if the tail is
6107    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6108    /// WITH DATA).
6109    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6110        let save = self.pos;
6111        // `WITH` is an Ident (not reserved in the lexer).
6112        let is_with = match self.peek() {
6113            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6114            _ => false,
6115        };
6116        if !is_with {
6117            return Ok(default_when_absent);
6118        }
6119        self.advance();
6120        // Optional `NO`.
6121        let mut with_data = true;
6122        let is_no = match self.peek() {
6123            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6124            _ => false,
6125        };
6126        if is_no {
6127            self.advance();
6128            with_data = false;
6129        }
6130        // Required `DATA` ident.
6131        let is_data = match self.peek() {
6132            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6133            _ => false,
6134        };
6135        if is_data {
6136            self.advance();
6137            Ok(with_data)
6138        } else {
6139            // Caller's WITH wasn't WITH-DATA — rewind so the outer
6140            // parser can interpret it.
6141            self.pos = save;
6142            Ok(default_when_absent)
6143        }
6144    }
6145
6146    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6147    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6148    /// All keyword prefixes have already been consumed; the flags
6149    /// say which were present.
6150    fn parse_create_view_after_keyword(
6151        &mut self,
6152        or_replace: bool,
6153        _materialized_unused: bool,
6154        temporary: bool,
6155    ) -> Result<Statement, ParseError> {
6156        let if_not_exists = self.parse_if_not_exists();
6157        let name = self.expect_ident_like()?;
6158        // Optional `(col, col, …)` rename list.
6159        let mut columns: Vec<String> = Vec::new();
6160        if matches!(self.peek(), Token::LParen) {
6161            self.advance();
6162            loop {
6163                let c = self.expect_ident_like()?;
6164                columns.push(c);
6165                if matches!(self.peek(), Token::Comma) {
6166                    self.advance();
6167                    continue;
6168                }
6169                if matches!(self.peek(), Token::RParen) {
6170                    self.advance();
6171                    break;
6172                }
6173                return Err(self.err(alloc::format!(
6174                    "expected , or ) in VIEW column list, got {:?}",
6175                    self.peek()
6176                )));
6177            }
6178        }
6179        // Required `AS`.
6180        if !matches!(self.peek(), Token::As) {
6181            return Err(self.err(alloc::format!(
6182                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6183                self.peek()
6184            )));
6185        }
6186        self.advance();
6187        // Body: a regular SELECT statement. v7.39 (round 151) — a
6188        // WITH-headed body is legal too (read-only CTEs only; the
6189        // engine rejects data-modifying ones with PG's message).
6190        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6191        // with the check-option clause, so WITH here heads the query.
6192        let body = if self.peek_is_with_kw() {
6193            self.advance();
6194            self.parse_nested_with_select()?
6195        } else {
6196            let body_stmt = self.parse_select_stmt()?;
6197            let Statement::Select(body) = body_stmt else {
6198                return Err(self.err(alloc::format!(
6199                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6200                )));
6201            };
6202            body
6203        };
6204        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6205        // The SELECT parser stops before a trailing WITH, so it lands here.
6206        let check_option = if matches!(self.peek(),
6207            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6208        {
6209            self.advance(); // WITH
6210            let opt = match self.peek() {
6211                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6212                    self.advance();
6213                    crate::ast::ViewCheckOption::Local
6214                }
6215                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6216                    self.advance();
6217                    crate::ast::ViewCheckOption::Cascaded
6218                }
6219                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6220                _ => crate::ast::ViewCheckOption::Cascaded,
6221            };
6222            if !matches!(self.peek(),
6223                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6224            {
6225                return Err(self.err(alloc::format!(
6226                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6227                    self.peek()
6228                )));
6229            }
6230            self.advance(); // CHECK
6231            if !matches!(self.peek(),
6232                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6233            {
6234                return Err(self.err(alloc::format!(
6235                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6236                    self.peek()
6237                )));
6238            }
6239            self.advance(); // OPTION
6240            Some(opt)
6241        } else {
6242            None
6243        };
6244        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6245            name,
6246            or_replace,
6247            if_not_exists,
6248            temporary,
6249            columns,
6250            body,
6251            check_option,
6252        }))
6253    }
6254
6255    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6256    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6257    /// consumed; `temporary` carries whether TEMPORARY was seen.
6258    fn parse_create_sequence_after_keyword(
6259        &mut self,
6260        temporary: bool,
6261    ) -> Result<Statement, ParseError> {
6262        let if_not_exists = self.parse_if_not_exists();
6263        let name = self.expect_ident_like()?;
6264        // Optional `AS data_type`.
6265        let data_type = if matches!(self.peek(), Token::As) {
6266            self.advance();
6267            Some(self.parse_sequence_data_type()?)
6268        } else {
6269            None
6270        };
6271        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6272        Ok(Statement::CreateSequence(
6273            crate::ast::CreateSequenceStatement {
6274                name,
6275                if_not_exists,
6276                temporary,
6277                data_type,
6278                options,
6279            },
6280        ))
6281    }
6282
6283    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6284    /// already been consumed; this is reached after `SEQUENCE`.
6285    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6286    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6287        use crate::ast::AlterDomainAction as A;
6288        let name = self.expect_ident_like()?;
6289        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6290        let kw = match self.peek() {
6291            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6292            Token::Drop => alloc::string::String::from("drop"),
6293            Token::Default => alloc::string::String::from("default"),
6294            other => {
6295                return Err(self.err(alloc::format!(
6296                    "expected an ALTER DOMAIN action, got {other:?}"
6297                )));
6298            }
6299        };
6300        let action = match kw.as_str() {
6301            "add" => {
6302                self.advance();
6303                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6304                {
6305                    self.advance();
6306                    Some(self.expect_ident_like()?)
6307                } else {
6308                    None
6309                };
6310                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6311                    return Err(self.err(alloc::format!(
6312                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6313                        self.peek()
6314                    )));
6315                }
6316                self.advance();
6317                if !matches!(self.peek(), Token::LParen) {
6318                    return Err(self.err("expected '(' after CHECK".into()));
6319                }
6320                self.advance();
6321                let check = self.parse_expr(0)?;
6322                if !matches!(self.peek(), Token::RParen) {
6323                    return Err(self.err("expected ')' after CHECK expression".into()));
6324                }
6325                self.advance();
6326                A::AddConstraint { name: cname, check }
6327            }
6328            "drop" => {
6329                self.advance();
6330                match self.peek() {
6331                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6332                        self.advance();
6333                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6334                        {
6335                            self.advance();
6336                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6337                            {
6338                                return Err(self.err("expected EXISTS after IF".into()));
6339                            }
6340                            self.advance();
6341                            true
6342                        } else {
6343                            false
6344                        };
6345                        let cn = self.expect_ident_like()?;
6346                        A::DropConstraint {
6347                            name: cn,
6348                            if_exists,
6349                        }
6350                    }
6351                    Token::Default => {
6352                        self.advance();
6353                        A::DropDefault
6354                    }
6355                    Token::Not => {
6356                        self.advance();
6357                        if !matches!(self.peek(), Token::Null) {
6358                            return Err(self.err("expected NULL after NOT".into()));
6359                        }
6360                        self.advance();
6361                        A::DropNotNull
6362                    }
6363                    other => {
6364                        return Err(self.err(alloc::format!(
6365                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6366                        )));
6367                    }
6368                }
6369            }
6370            "set" => {
6371                self.advance();
6372                match self.peek() {
6373                    Token::Default => {
6374                        self.advance();
6375                        A::SetDefault(self.parse_expr(0)?)
6376                    }
6377                    Token::Not => {
6378                        self.advance();
6379                        if !matches!(self.peek(), Token::Null) {
6380                            return Err(self.err("expected NULL after NOT".into()));
6381                        }
6382                        self.advance();
6383                        A::SetNotNull
6384                    }
6385                    other => {
6386                        return Err(self.err(alloc::format!(
6387                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6388                        )));
6389                    }
6390                }
6391            }
6392            "rename" => {
6393                self.advance();
6394                if !matches!(self.peek(), Token::To) {
6395                    return Err(self.err("expected TO after RENAME".into()));
6396                }
6397                self.advance();
6398                A::RenameTo(self.expect_ident_like()?)
6399            }
6400            other => {
6401                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6402            }
6403        };
6404        Ok(Statement::AlterDomain { name, action })
6405    }
6406
6407    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6408        let if_exists = self.parse_if_exists();
6409        let name = self.expect_ident_like()?;
6410        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6411        // the option list (PG allows only one or the other).
6412        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6413            self.advance();
6414            if matches!(self.peek(), Token::To) {
6415                self.advance();
6416            } else {
6417                self.expect_keyword_ident("to")?;
6418            }
6419            let new = self.expect_ident_like()?;
6420            return Ok(Statement::AlterSequence(
6421                crate::ast::AlterSequenceStatement {
6422                    name,
6423                    if_exists,
6424                    options: crate::ast::SequenceOptions::default(),
6425                    rename_to: Some(new),
6426                },
6427            ));
6428        }
6429        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6430        Ok(Statement::AlterSequence(
6431            crate::ast::AlterSequenceStatement {
6432                name,
6433                if_exists,
6434                options,
6435                rename_to: None,
6436            },
6437        ))
6438    }
6439
6440    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6441        let kw = self.expect_ident_like()?;
6442        match kw.to_ascii_lowercase().as_str() {
6443            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6444            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6445            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6446            other => Err(self.err(alloc::format!(
6447                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6448            ))),
6449        }
6450    }
6451
6452    fn parse_sequence_options(
6453        &mut self,
6454        allow_restart: bool,
6455    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6456        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6457        let mut opts = SequenceOptions::default();
6458        #[allow(clippy::while_let_loop)]
6459        loop {
6460            // Match an ident; stop at any non-ident token (sentinel,
6461            // semicolon, end of statement).
6462            let kw_lc = match self.peek() {
6463                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6464                _ => break,
6465            };
6466            match kw_lc.as_str() {
6467                "increment" => {
6468                    self.advance();
6469                    // Optional BY.
6470                    if self.peek_is_by() {
6471                        self.advance();
6472                    }
6473                    opts.increment = Some(self.expect_signed_int()?);
6474                }
6475                "minvalue" => {
6476                    self.advance();
6477                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6478                }
6479                "maxvalue" => {
6480                    self.advance();
6481                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6482                }
6483                "no" => {
6484                    self.advance();
6485                    let what = self.expect_ident_like()?;
6486                    match what.to_ascii_lowercase().as_str() {
6487                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6488                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6489                        "cycle" => opts.cycle = Some(false),
6490                        other => {
6491                            return Err(self.err(alloc::format!(
6492                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6493                            )));
6494                        }
6495                    }
6496                }
6497                "start" => {
6498                    self.advance();
6499                    // Optional WITH.
6500                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6501                        if s.eq_ignore_ascii_case("with"))
6502                    {
6503                        self.advance();
6504                    }
6505                    opts.start = Some(self.expect_signed_int()?);
6506                }
6507                "restart" if allow_restart => {
6508                    self.advance();
6509                    // Optional WITH n; bare RESTART means restart at START.
6510                    let mut with_val: Option<i64> = None;
6511                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6512                        if s.eq_ignore_ascii_case("with"))
6513                    {
6514                        self.advance();
6515                        with_val = Some(self.expect_signed_int()?);
6516                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6517                        with_val = Some(self.expect_signed_int()?);
6518                    }
6519                    opts.restart = Some(with_val);
6520                }
6521                "cache" => {
6522                    self.advance();
6523                    opts.cache = Some(self.expect_signed_int()?);
6524                }
6525                "cycle" => {
6526                    self.advance();
6527                    opts.cycle = Some(true);
6528                }
6529                "owned" => {
6530                    self.advance();
6531                    match self.peek() {
6532                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6533                            self.advance();
6534                        }
6535                        other => {
6536                            return Err(
6537                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6538                            );
6539                        }
6540                    }
6541                    // OWNED BY {NONE | tab.col}. Read just one ident
6542                    // (NOT expect_ident_like which would auto-strip
6543                    // a schema prefix and consume the `.col` we need).
6544                    let first = match self.advance() {
6545                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6546                        other => {
6547                            return Err(self.err(alloc::format!(
6548                                "expected identifier or NONE after OWNED BY, got {other:?}"
6549                            )));
6550                        }
6551                    };
6552                    if first.eq_ignore_ascii_case("none") {
6553                        opts.owned_by = Some(SequenceOwnedBy::None);
6554                    } else if matches!(self.peek(), Token::Dot) {
6555                        self.advance();
6556                        let second = match self.advance() {
6557                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6558                            other => {
6559                                return Err(self.err(alloc::format!(
6560                                    "expected column name after OWNED BY {first}., got {other:?}"
6561                                )));
6562                            }
6563                        };
6564                        // v7.17 dump-compat fix — pg_dump emits
6565                        // OWNED BY clauses as
6566                        // `schema.table.column` (three segments).
6567                        // If a third `.<ident>` follows, treat the
6568                        // first ident as schema (drop it; SPG is
6569                        // single-schema) and the middle / last
6570                        // pair as table.column. Otherwise it's
6571                        // the two-segment form table.column.
6572                        if matches!(self.peek(), Token::Dot) {
6573                            self.advance();
6574                            let third = match self.advance() {
6575                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6576                                other => {
6577                                    return Err(self.err(alloc::format!(
6578                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6579                                    )));
6580                                }
6581                            };
6582                            let _ = first; // schema prefix discarded
6583                            opts.owned_by = Some(SequenceOwnedBy::Column {
6584                                table: second,
6585                                column: third,
6586                            });
6587                        } else {
6588                            opts.owned_by = Some(SequenceOwnedBy::Column {
6589                                table: first,
6590                                column: second,
6591                            });
6592                        }
6593                    } else {
6594                        return Err(self.err(alloc::format!(
6595                            "expected table.column or NONE after OWNED BY, got {first:?}"
6596                        )));
6597                    }
6598                }
6599                _ => break,
6600            }
6601        }
6602        Ok(opts)
6603    }
6604
6605    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6606        let neg = if matches!(self.peek(), Token::Minus) {
6607            self.advance();
6608            true
6609        } else {
6610            false
6611        };
6612        match self.peek() {
6613            Token::Integer(n) => {
6614                let v = *n;
6615                self.advance();
6616                Ok(if neg { -v } else { v })
6617            }
6618            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6619        }
6620    }
6621
6622    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6623    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6624    /// clause is fully accepted and discarded — SPG always runs
6625    /// constraint checks immediately (single-writer model). The
6626    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6627    /// in either order (per the SQL spec they're independent),
6628    /// though pg_dump always emits them in the canonical
6629    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6630    /// Stops at the first token that isn't part of the clause.
6631    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6632        self.consume_deferrable_clauses_timed().map(|_| ())
6633    }
6634
6635    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6636    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6637    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6638    /// NOT DEFERRABLE and a circular-FK migration could not load.
6639    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6640        let mut deferrable = false;
6641        let mut initially_deferred = false;
6642        loop {
6643            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6644            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6645                self.advance();
6646                deferrable = true;
6647                if self.consume_optional_initially_clause()? {
6648                    initially_deferred = true;
6649                }
6650                continue;
6651            }
6652            // `NOT DEFERRABLE` — already worked pre-3.1.
6653            if matches!(self.peek(), Token::Not) {
6654                let look = self.tokens.get(self.pos + 1);
6655                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6656                    self.advance(); // NOT
6657                    self.advance(); // DEFERRABLE
6658                    deferrable = false;
6659                    initially_deferred = false;
6660                    let _ = self.consume_optional_initially_clause()?;
6661                    continue;
6662                }
6663                break;
6664            }
6665            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6666            // accepts this without a leading [NOT] DEFERRABLE
6667            // (the timing keyword alone). pg_dump occasionally
6668            // emits it on FK constraints that inherit timing.
6669            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6670                if self.consume_optional_initially_clause()? {
6671                    initially_deferred = true;
6672                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6673                    deferrable = true;
6674                }
6675                continue;
6676            }
6677            break;
6678        }
6679        Ok((deferrable, initially_deferred))
6680    }
6681
6682    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6683    /// next token is `INITIALLY`, consume it plus the required
6684    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6685    /// Returns true when the timing seen was `DEFERRED`.
6686    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6687        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6688            return Ok(false);
6689        }
6690        self.advance(); // INITIALLY
6691        match self.advance() {
6692            Token::Ident(s)
6693                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6694            {
6695                Ok(s.eq_ignore_ascii_case("deferred"))
6696            }
6697            other => Err(self.err(alloc::format!(
6698                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6699            ))),
6700        }
6701    }
6702
6703    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6704    /// in its entirety so the parser returns Empty without
6705    /// touching the runtime. The CREATE+PROCEDURE keywords are
6706    /// already consumed; this swallows everything from the
6707    /// procedure name through the matching `END`, including
6708    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6709    /// (DELIMITER `//` makes the script splitter forward the
6710    /// whole block as one statement), `@var` session-variable
6711    /// references, and the trailing terminator.
6712    ///
6713    /// Tracks nesting depth so:
6714    ///   BEGIN
6715    ///     IF cond THEN
6716    ///       BEGIN ... END;
6717    ///     END IF;
6718    ///   END
6719    /// terminates at the outer END.
6720    fn consume_mysql_routine_body(&mut self) {
6721        // Outer skeleton: name, (...), optional clauses, BEGIN
6722        // <body> END [;]. Scan for the first BEGIN — anything
6723        // before it is signature decoration we don't care about.
6724        // Once inside BEGIN, count up on BEGIN, down on END.
6725        let mut depth: i32 = 0;
6726        let mut started = false;
6727        loop {
6728            match self.peek().clone() {
6729                Token::Begin => {
6730                    self.advance();
6731                    depth += 1;
6732                    started = true;
6733                }
6734                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6735                    self.advance();
6736                    if started {
6737                        depth -= 1;
6738                        if depth <= 0 {
6739                            // Optional trailing ident (`END IF`,
6740                            // `END LOOP`, `END WHILE`, `END CASE`,
6741                            // `END label_name`) — eat the next
6742                            // ident if present so we don't
6743                            // mistake `END IF;` for the outer
6744                            // close.
6745                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6746                                // If the next token is one of the
6747                                // PL/SQL block-closer keywords,
6748                                // the END belongs to an inner
6749                                // block; bump depth back up.
6750                                let is_inner_close = matches!(
6751                                    self.peek(),
6752                                    Token::Ident(s) | Token::QuotedIdent(s)
6753                                        if matches!(
6754                                            s.to_ascii_lowercase().as_str(),
6755                                            "if" | "loop" | "while" | "case" | "repeat"
6756                                        )
6757                                );
6758                                if is_inner_close {
6759                                    self.advance();
6760                                    depth += 1;
6761                                    continue;
6762                                }
6763                            }
6764                            // Eat optional trailing `;`.
6765                            if matches!(self.peek(), Token::Semicolon) {
6766                                self.advance();
6767                            }
6768                            return;
6769                        }
6770                    }
6771                }
6772                Token::Eof => return,
6773                _ => {
6774                    self.advance();
6775                }
6776            }
6777        }
6778    }
6779
6780    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6781    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6782    ///
6783    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6784    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6785    ///   ident, or `ident @ ident-or-quoted-string` host form)
6786    /// * `SQL SECURITY {DEFINER|INVOKER}`
6787    ///
6788    /// Each clause may appear at most once but in any order.
6789    /// The hints are pure planner / permission metadata that
6790    /// SPG's view-rewrite engine handles uniformly; we accept
6791    /// and discard. Returns `Ok(())` once a non-clause token is
6792    /// peeked (the caller then checks for the `VIEW` keyword).
6793    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6794        loop {
6795            match self.peek().clone() {
6796                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6797                    self.advance(); // ALGORITHM
6798                    // Optional `=`. MySQL spec requires it but be
6799                    // generous.
6800                    if matches!(self.peek(), Token::Eq) {
6801                        self.advance();
6802                    }
6803                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6804                    // bare ident; unknown values still parse so
6805                    // future MySQL versions don't break.
6806                    if matches!(
6807                        self.peek(),
6808                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6809                    ) {
6810                        self.advance();
6811                    }
6812                }
6813                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6814                    self.advance(); // DEFINER
6815                    if matches!(self.peek(), Token::Eq) {
6816                        self.advance();
6817                    }
6818                    // User: quoted string, ident, OR ident @ host
6819                    // (host may itself be quoted or bare).
6820                    match self.peek().clone() {
6821                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6822                            self.advance();
6823                            // Optional `@host`.
6824                            if matches!(self.peek(), Token::At) {
6825                                self.advance();
6826                                if matches!(
6827                                    self.peek(),
6828                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6829                                ) {
6830                                    self.advance();
6831                                }
6832                            }
6833                        }
6834                        _ => {}
6835                    }
6836                }
6837                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6838                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6839                    // when followed by SECURITY — the dispatcher must
6840                    // not consume a bare `SQL` token (it's not a
6841                    // legal CREATE prefix on its own).
6842                    let save = self.pos;
6843                    self.advance(); // SQL
6844                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6845                        if s2.eq_ignore_ascii_case("security"))
6846                    {
6847                        self.advance(); // SECURITY
6848                        // DEFINER / INVOKER trailing ident.
6849                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6850                            self.advance();
6851                        }
6852                    } else {
6853                        // Not a SQL SECURITY clause — roll back and
6854                        // bail; the caller will error out cleanly.
6855                        self.pos = save;
6856                        return Ok(());
6857                    }
6858                }
6859                _ => return Ok(()),
6860            }
6861        }
6862    }
6863
6864    fn parse_if_not_exists(&mut self) -> bool {
6865        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6866        {
6867            let save = self.pos;
6868            self.advance();
6869            if matches!(self.peek(), Token::Not) {
6870                self.advance();
6871                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6872                {
6873                    self.advance();
6874                    return true;
6875                }
6876            }
6877            self.pos = save;
6878        }
6879        false
6880    }
6881
6882    fn parse_if_exists(&mut self) -> bool {
6883        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6884        {
6885            let save = self.pos;
6886            self.advance();
6887            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6888            {
6889                self.advance();
6890                return true;
6891            }
6892            self.pos = save;
6893        }
6894        false
6895    }
6896
6897    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6898    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6899    /// been consumed.
6900    fn parse_create_trigger_after_keyword(
6901        &mut self,
6902        or_replace: bool,
6903    ) -> Result<Statement, ParseError> {
6904        let name = self.expect_ident_like()?;
6905        let timing = {
6906            let ident = self.expect_ident_like()?;
6907            if ident.eq_ignore_ascii_case("before") {
6908                TriggerTiming::Before
6909            } else if ident.eq_ignore_ascii_case("after") {
6910                TriggerTiming::After
6911            } else if ident.eq_ignore_ascii_case("instead") {
6912                let next = self.expect_ident_like()?;
6913                if !next.eq_ignore_ascii_case("of") {
6914                    return Err(self.err(alloc::format!(
6915                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6916                    )));
6917                }
6918                TriggerTiming::InsteadOf
6919            } else {
6920                return Err(self.err(alloc::format!(
6921                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6922                )));
6923            }
6924        };
6925        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6926        // OR is a reserved keyword token (Token::Or), not an Ident.
6927        // v7.13.0 — after an UPDATE event we may optionally see
6928        // `OF col, col, …` (mailrs round-5 G7). Columns are
6929        // captured into `update_columns` once across the whole
6930        // events list; multiple `UPDATE OF` clauses are rejected.
6931        let mut events: Vec<TriggerEvent> = Vec::new();
6932        let mut update_columns: Vec<String> = Vec::new();
6933        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6934        events.push(first_ev);
6935        if !first_cols.is_empty() {
6936            update_columns = first_cols;
6937        }
6938        while matches!(self.peek(), Token::Or) {
6939            self.advance();
6940            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6941            events.push(ev);
6942            if !cols.is_empty() {
6943                if !update_columns.is_empty() {
6944                    return Err(
6945                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6946                    );
6947                }
6948                update_columns = cols;
6949            }
6950        }
6951        // ON <table>
6952        let tok = self.peek();
6953        let Token::On = tok else {
6954            return Err(self.err(alloc::format!(
6955                "expected ON after trigger events, got {tok:?}"
6956            )));
6957        };
6958        self.advance();
6959        let table = self.expect_ident_like()?;
6960        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6961        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6962        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6963        // the trigger as a plain AFTER trigger (correct for every non-deferred
6964        // use; deferral timing is not yet honoured).
6965        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6966            if s.eq_ignore_ascii_case("from"))
6967        {
6968            self.advance();
6969            let _reftable = self.expect_ident_like()?;
6970        }
6971        self.consume_optional_deferrable_clauses()?;
6972        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6973        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6974        // idents.
6975        if !matches!(self.peek(), Token::For) {
6976            return Err(self.err(alloc::format!(
6977                "expected FOR EACH ROW / STATEMENT, got {:?}",
6978                self.peek()
6979            )));
6980        }
6981        self.advance();
6982        let for_each = {
6983            let e = self.expect_ident_like()?;
6984            if !e.eq_ignore_ascii_case("each") {
6985                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
6986            }
6987            let unit = self.expect_ident_like()?;
6988            if unit.eq_ignore_ascii_case("row") {
6989                TriggerForEach::Row
6990            } else if unit.eq_ignore_ascii_case("statement") {
6991                TriggerForEach::Statement
6992            } else {
6993                return Err(self.err(alloc::format!(
6994                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
6995                )));
6996            }
6997        };
6998        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
6999        let when_condition = if matches!(self.peek(),
7000            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7001        {
7002            self.advance();
7003            Some(self.parse_paren_expr("WHEN")?)
7004        } else {
7005            None
7006        };
7007        // EXECUTE FUNCTION/PROCEDURE name(...)
7008        let exec = self.expect_ident_like()?;
7009        if !exec.eq_ignore_ascii_case("execute") {
7010            return Err(self.err(alloc::format!(
7011                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
7012            )));
7013        }
7014        let fn_or_proc = self.expect_ident_like()?;
7015        if !(fn_or_proc.eq_ignore_ascii_case("function")
7016            || fn_or_proc.eq_ignore_ascii_case("procedure"))
7017        {
7018            return Err(self.err(alloc::format!(
7019                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
7020            )));
7021        }
7022        let function = self.expect_ident_like()?;
7023        // Optional empty arg list `()`.
7024        if matches!(self.peek(), Token::LParen) {
7025            self.advance();
7026            if !matches!(self.peek(), Token::RParen) {
7027                return Err(self.err(alloc::format!(
7028                    "v7.12.4 trigger function calls take no args; got {:?}",
7029                    self.peek()
7030                )));
7031            }
7032            self.advance();
7033        }
7034        Ok(Statement::CreateTrigger(CreateTriggerStatement {
7035            name,
7036            or_replace,
7037            timing,
7038            events,
7039            table,
7040            for_each,
7041            function,
7042            update_columns,
7043            when_condition,
7044        }))
7045    }
7046
7047    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
7048    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
7049    fn parse_create_rule_after_keyword(
7050        &mut self,
7051        or_replace: bool,
7052    ) -> Result<Statement, ParseError> {
7053        let name = self.expect_ident_like()?;
7054        if !matches!(self.peek(), Token::As) {
7055            return Err(self.err(alloc::format!(
7056                "expected AS in CREATE RULE, got {:?}",
7057                self.peek()
7058            )));
7059        }
7060        self.advance();
7061        if !matches!(self.peek(), Token::On) {
7062            return Err(self.err(alloc::format!(
7063                "expected ON in CREATE RULE, got {:?}",
7064                self.peek()
7065            )));
7066        }
7067        self.advance();
7068        let event = self.parse_rule_event()?;
7069        if !matches!(self.peek(), Token::To)
7070            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
7071        {
7072            return Err(self.err(alloc::format!(
7073                "expected TO after rule event, got {:?}",
7074                self.peek()
7075            )));
7076        }
7077        self.advance();
7078        let table = self.expect_ident_like()?;
7079        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7080        let when_condition = if matches!(self.peek(), Token::Where) {
7081            self.advance();
7082            Some(self.parse_expr(0)?)
7083        } else {
7084            None
7085        };
7086        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7087        {
7088            return Err(self.err(alloc::format!(
7089                "expected DO in CREATE RULE, got {:?}",
7090                self.peek()
7091            )));
7092        }
7093        self.advance();
7094        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7095        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7096        {
7097            self.advance();
7098            true
7099        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7100            self.advance();
7101            false
7102        } else {
7103            false
7104        };
7105        // `NOTHING` | `( cmd; … )` | `cmd`.
7106        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7107        {
7108            self.advance();
7109            Vec::new()
7110        } else if matches!(self.peek(), Token::LParen) {
7111            self.advance();
7112            let mut cmds = Vec::new();
7113            loop {
7114                cmds.push(self.parse_one_statement()?);
7115                if matches!(self.peek(), Token::Semicolon) {
7116                    self.advance();
7117                    if matches!(self.peek(), Token::RParen) {
7118                        break;
7119                    }
7120                    continue;
7121                }
7122                break;
7123            }
7124            if !matches!(self.peek(), Token::RParen) {
7125                return Err(self.err(alloc::format!(
7126                    "expected ) closing the CREATE RULE command list, got {:?}",
7127                    self.peek()
7128                )));
7129            }
7130            self.advance();
7131            cmds
7132        } else {
7133            alloc::vec![self.parse_one_statement()?]
7134        };
7135        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7136            name,
7137            or_replace,
7138            event,
7139            table,
7140            instead,
7141            when_condition,
7142            commands,
7143        }))
7144    }
7145
7146    /// v7.39 (round 139) — a rule event keyword → uppercase string.
7147    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7148        if matches!(self.peek(), Token::Insert) {
7149            self.advance();
7150            return Ok(alloc::string::String::from("INSERT"));
7151        }
7152        if matches!(self.peek(), Token::Select) {
7153            self.advance();
7154            return Ok(alloc::string::String::from("SELECT"));
7155        }
7156        match self.peek() {
7157            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7158                self.advance();
7159                Ok(alloc::string::String::from("UPDATE"))
7160            }
7161            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7162                self.advance();
7163                Ok(alloc::string::String::from("DELETE"))
7164            }
7165            other => Err(self.err(alloc::format!(
7166                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7167            ))),
7168        }
7169    }
7170
7171    /// v7.13.0 — parse one trigger event, then optionally consume
7172    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7173    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7174    fn parse_trigger_event_with_optional_of(
7175        &mut self,
7176    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7177        let ev = self.parse_trigger_event()?;
7178        if !matches!(ev, TriggerEvent::Update) {
7179            return Ok((ev, Vec::new()));
7180        }
7181        // `OF` is a bare ident.
7182        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7183            return Ok((ev, Vec::new()));
7184        }
7185        self.advance(); // OF
7186        let mut cols: Vec<String> = Vec::new();
7187        loop {
7188            cols.push(self.expect_ident_like()?);
7189            if matches!(self.peek(), Token::Comma) {
7190                self.advance();
7191                continue;
7192            }
7193            break;
7194        }
7195        if cols.is_empty() {
7196            return Err(
7197                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7198            );
7199        }
7200        Ok((ev, cols))
7201    }
7202
7203    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7204    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7205    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7206    /// inside the body.
7207    /// Called by [`parse_plpgsql_body`] after the body's tokens
7208    /// have been lexed into this temporary parser.
7209    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7210        // v7.12.6 — optional DECLARE prelude.
7211        let declarations = if matches!(
7212            self.peek(),
7213            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7214        ) {
7215            self.advance();
7216            self.parse_plpgsql_declare_block()?
7217        } else {
7218            Vec::new()
7219        };
7220        // BEGIN keyword (PL/pgSQL — distinct from the SQL
7221        // `BEGIN` transaction-start, but we can reuse the
7222        // reserved Token::Begin since the body is a separate
7223        // lex/parse context).
7224        if !matches!(self.peek(), Token::Begin) {
7225            return Err(self.err(alloc::format!(
7226                "expected BEGIN at start of plpgsql block, got {:?}",
7227                self.peek()
7228            )));
7229        }
7230        self.advance();
7231        let statements = self.parse_plpgsql_stmt_list_until_end()?;
7232        // v7.37.20 (20.10) — optional EXCEPTION clause between the
7233        // body's last statement and the trailing END. When present
7234        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7235        // arms terminated by END.
7236        let exception_handlers = if matches!(
7237            self.peek(),
7238            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7239        ) {
7240            self.advance();
7241            self.parse_plpgsql_exception_handlers()?
7242        } else {
7243            Vec::new()
7244        };
7245        Ok(PlPgSqlBlock {
7246            declarations,
7247            statements,
7248            exception_handlers,
7249        })
7250    }
7251
7252    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7253    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7254    fn parse_plpgsql_exception_handlers(
7255        &mut self,
7256    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7257        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7258        loop {
7259            // Stop at END — the block-level trailing END LOOP / END;
7260            // is handled by the caller.
7261            if matches!(
7262                self.peek(),
7263                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7264            ) {
7265                return Ok(out);
7266            }
7267            // WHEN <cond> [OR <cond>]* THEN <body>
7268            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7269            {
7270                return Err(self.err(alloc::format!(
7271                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
7272                    self.peek()
7273                )));
7274            }
7275            self.advance();
7276            let mut conditions: Vec<String> = Vec::new();
7277            conditions.push(self.expect_ident_like()?);
7278            while matches!(self.peek(), Token::Or) {
7279                self.advance();
7280                conditions.push(self.expect_ident_like()?);
7281            }
7282            let then_kw = self.expect_ident_like()?;
7283            if !then_kw.eq_ignore_ascii_case("then") {
7284                return Err(self.err(alloc::format!(
7285                    "expected THEN after WHEN condition list, got {then_kw:?}"
7286                )));
7287            }
7288            let body = self.parse_plpgsql_stmt_list_until_end()?;
7289            out.push(crate::ast::ExceptionHandler { conditions, body });
7290        }
7291    }
7292
7293    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7294    /// prelude. Caller has already consumed `DECLARE`. We stop
7295    /// reading entries when we hit `BEGIN`.
7296    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7297        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7298        loop {
7299            if matches!(self.peek(), Token::Begin) {
7300                return Ok(out);
7301            }
7302            let name = self.expect_ident_like()?;
7303            // v7.37.20 (20.7) — type inference: if the next token is
7304            // `:=` or `=` (no explicit type), infer from the default
7305            // expression. Otherwise the ident that follows is the
7306            // declared type.
7307            //
7308            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7309            // (PG-standard). SPG parse-accepts and treats identically
7310            // to inference — the eventual runtime value determines
7311            // the local's type, which is faithful to how SPG handles
7312            // untyped locals today (see 20.7). Full compile-time
7313            // catalog lookup queues with v7.40 PL/pgSQL epic.
7314            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7315                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7316                // downstream declaration walker to type the local by
7317                // the runtime type of the default expression.
7318                FunctionArgType::Raw("_infer_".into())
7319            } else {
7320                let ty_token = self.expect_ident_like()?;
7321                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7322                // consume optional `.<ident>` qualifier + `%<KW>`
7323                // suffix. Both qualifier and suffix map to _infer_.
7324                if matches!(self.peek(), Token::Dot) {
7325                    self.advance();
7326                    let _ = self.expect_ident_like()?;
7327                }
7328                if matches!(self.peek(), Token::Percent) {
7329                    self.advance();
7330                    // Consume the trailing TYPE / ROWTYPE ident.
7331                    let _ = self.expect_ident_like()?;
7332                    FunctionArgType::Raw("_infer_".into())
7333                } else {
7334                    match map_type_ident_to_column_type_name(&ty_token) {
7335                        Some(t) => FunctionArgType::Typed(t),
7336                        None => FunctionArgType::Raw(ty_token),
7337                    }
7338                }
7339            };
7340            let default = match self.peek() {
7341                Token::ColonEq => {
7342                    self.advance();
7343                    Some(self.parse_expr(0)?)
7344                }
7345                Token::Eq => {
7346                    // PL/pgSQL also accepts `=` for the
7347                    // DECLARE default (PG treats them the same
7348                    // in this position).
7349                    self.advance();
7350                    Some(self.parse_expr(0)?)
7351                }
7352                _ => None,
7353            };
7354            // Mandatory `;` between declarations.
7355            if !matches!(self.peek(), Token::Semicolon) {
7356                return Err(self.err(alloc::format!(
7357                    "expected ; after DECLARE entry for {name:?}, got {:?}",
7358                    self.peek()
7359                )));
7360            }
7361            self.advance();
7362            out.push(PlPgSqlDeclare { name, ty, default });
7363        }
7364    }
7365
7366    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7367    /// the terminating `END;` (or `END IF;` etc — handled by the
7368    /// per-construct sub-parsers). Used by both the outer block
7369    /// and the IF/ELSE branch bodies.
7370    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7371        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7372        loop {
7373            // Allow trailing semicolons + END.
7374            while matches!(self.peek(), Token::Semicolon) {
7375                self.advance();
7376            }
7377            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7378            if matches!(
7379                self.peek(),
7380                Token::Ident(s) | Token::QuotedIdent(s)
7381                    if s.eq_ignore_ascii_case("end")
7382                        || s.eq_ignore_ascii_case("else")
7383                        || s.eq_ignore_ascii_case("elsif")
7384                        || s.eq_ignore_ascii_case("elseif")
7385                        || s.eq_ignore_ascii_case("exception")
7386                        || s.eq_ignore_ascii_case("when")
7387            ) {
7388                return Ok(statements);
7389            }
7390            // Otherwise: one statement, then expect `;` or
7391            // a block-terminator keyword.
7392            let stmt = self.parse_plpgsql_stmt()?;
7393            statements.push(stmt);
7394            match self.peek() {
7395                Token::Semicolon => {
7396                    self.advance();
7397                }
7398                Token::Ident(s) | Token::QuotedIdent(s)
7399                    if s.eq_ignore_ascii_case("end")
7400                        || s.eq_ignore_ascii_case("else")
7401                        || s.eq_ignore_ascii_case("elsif")
7402                        || s.eq_ignore_ascii_case("elseif")
7403                        || s.eq_ignore_ascii_case("exception")
7404                        || s.eq_ignore_ascii_case("when") =>
7405                {
7406                    // Final statement of the block without `;`.
7407                }
7408                other => {
7409                    return Err(self.err(alloc::format!(
7410                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7411                    )));
7412                }
7413            }
7414        }
7415    }
7416
7417    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7418        // RETURN keyword?
7419        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7420        {
7421            self.advance();
7422            return self.parse_plpgsql_return();
7423        }
7424        // v7.12.6 — IF block.
7425        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7426        {
7427            self.advance();
7428            return self.parse_plpgsql_if();
7429        }
7430        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7431        // Detected by peeking that token pos+3 is Ident("execute").
7432        if matches!(self.peek(), Token::For)
7433            && matches!(
7434                self.tokens.get(self.pos + 1),
7435                Some(Token::Ident(_) | Token::QuotedIdent(_))
7436            )
7437            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7438            && matches!(
7439                self.tokens.get(self.pos + 3),
7440                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7441            )
7442        {
7443            self.advance(); // FOR
7444            let var = self.expect_ident_like()?;
7445            self.advance(); // IN
7446            self.advance(); // EXECUTE
7447            // Prescan for LOOP at paren depth 0 so parse_expr stops
7448            // before the LOOP keyword (same trick as the bare-SELECT
7449            // ForQuery arm).
7450            let mut depth: i32 = 0;
7451            let mut loop_pos: Option<usize> = None;
7452            let mut scan = self.pos;
7453            while scan < self.tokens.len() {
7454                match self.tokens.get(scan) {
7455                    Some(Token::LParen) => depth += 1,
7456                    Some(Token::RParen) => depth -= 1,
7457                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7458                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7459                    {
7460                        loop_pos = Some(scan);
7461                        break;
7462                    }
7463                    _ => {}
7464                }
7465                scan += 1;
7466            }
7467            let loop_pos = loop_pos.ok_or_else(|| {
7468                self.err(alloc::format!(
7469                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7470                ))
7471            })?;
7472            let saved_loop = self.tokens[loop_pos].clone();
7473            self.tokens[loop_pos] = Token::Semicolon;
7474            let expr_result = self.parse_expr(0);
7475            self.tokens[loop_pos] = saved_loop;
7476            let sql_expr = expr_result?;
7477            let loop_kw = self.expect_ident_like()?;
7478            if !loop_kw.eq_ignore_ascii_case("loop") {
7479                return Err(self.err(alloc::format!(
7480                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7481                )));
7482            }
7483            let body = self.parse_plpgsql_stmt_list_until_end()?;
7484            let end_kw = self.expect_ident_like()?;
7485            if !end_kw.eq_ignore_ascii_case("end") {
7486                return Err(self.err(alloc::format!(
7487                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7488                )));
7489            }
7490            let loop_kw2 = self.expect_ident_like()?;
7491            if !loop_kw2.eq_ignore_ascii_case("loop") {
7492                return Err(self.err(alloc::format!(
7493                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7494                )));
7495            }
7496            return Ok(PlPgSqlStmt::ForExecute {
7497                var,
7498                sql_expr,
7499                body,
7500            });
7501        }
7502        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7503        //
7504        // Two syntactic forms:
7505        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7506        //   FOR var IN (SELECT ...) LOOP ...
7507        //
7508        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7509        // the trailing `LOOP` keyword as a table alias, we prescan
7510        // forward to find LOOP at paren depth 0, splice a fake
7511        // Semicolon at that position (so SELECT parses cleanly),
7512        // then re-splice LOOP back in.
7513        //
7514        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7515        // LOOP directly — no scan required.
7516        if matches!(self.peek(), Token::For)
7517            && matches!(
7518                self.tokens.get(self.pos + 1),
7519                Some(Token::Ident(_) | Token::QuotedIdent(_))
7520            )
7521            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7522            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7523                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7524        {
7525            self.advance(); // FOR
7526            let var = self.expect_ident_like()?;
7527            // IN
7528            self.advance();
7529            let query = if matches!(self.peek(), Token::LParen) {
7530                // Paren-wrapped SELECT.
7531                self.advance();
7532                let inner = self.parse_select_stmt()?;
7533                let Statement::Select(q) = inner else {
7534                    return Err(self.err(alloc::format!(
7535                        "expected SELECT inside (…), got {:?}",
7536                        self.peek()
7537                    )));
7538                };
7539                if !matches!(self.peek(), Token::RParen) {
7540                    return Err(self.err(alloc::format!(
7541                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7542                        self.peek()
7543                    )));
7544                }
7545                self.advance();
7546                q
7547            } else {
7548                // Bare SELECT: prescan to find the LOOP boundary.
7549                let mut depth: i32 = 0;
7550                let mut loop_pos: Option<usize> = None;
7551                let mut scan = self.pos;
7552                while scan < self.tokens.len() {
7553                    match self.tokens.get(scan) {
7554                        Some(Token::LParen) => depth += 1,
7555                        Some(Token::RParen) => depth -= 1,
7556                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7557                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7558                        {
7559                            loop_pos = Some(scan);
7560                            break;
7561                        }
7562                        _ => {}
7563                    }
7564                    scan += 1;
7565                }
7566                let loop_pos = loop_pos.ok_or_else(|| {
7567                    self.err(alloc::format!(
7568                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7569                    ))
7570                })?;
7571                // Swap the LOOP token with a synthetic Semicolon so
7572                // parse_select_stmt stops there, then restore afterward.
7573                let saved_loop = self.tokens[loop_pos].clone();
7574                self.tokens[loop_pos] = Token::Semicolon;
7575                let parse_result = self.parse_select_stmt();
7576                self.tokens[loop_pos] = saved_loop;
7577                let inner = parse_result?;
7578                let Statement::Select(q) = inner else {
7579                    return Err(self.err(alloc::format!(
7580                        "expected SELECT after FOR <var> IN, got {:?}",
7581                        self.peek()
7582                    )));
7583                };
7584                q
7585            };
7586            let loop_kw = self.expect_ident_like()?;
7587            if !loop_kw.eq_ignore_ascii_case("loop") {
7588                return Err(self.err(alloc::format!(
7589                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7590                )));
7591            }
7592            let body = self.parse_plpgsql_stmt_list_until_end()?;
7593            let end_kw = self.expect_ident_like()?;
7594            if !end_kw.eq_ignore_ascii_case("end") {
7595                return Err(self.err(alloc::format!(
7596                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7597                )));
7598            }
7599            let loop_kw2 = self.expect_ident_like()?;
7600            if !loop_kw2.eq_ignore_ascii_case("loop") {
7601                return Err(self.err(alloc::format!(
7602                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7603                )));
7604            }
7605            return Ok(PlPgSqlStmt::ForQuery {
7606                var,
7607                query: Box::new(query),
7608                body,
7609            });
7610        }
7611        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7612        // FOR is a reserved keyword token (Token::For).
7613        if matches!(self.peek(), Token::For)
7614            && matches!(
7615                self.tokens.get(self.pos + 1),
7616                Some(Token::Ident(_) | Token::QuotedIdent(_))
7617            )
7618            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7619        {
7620            self.advance(); // FOR
7621            let var = self.expect_ident_like()?;
7622            if !matches!(self.peek(), Token::In) {
7623                return Err(self.err(alloc::format!(
7624                    "expected IN after FOR <var>, got {:?}",
7625                    self.peek()
7626                )));
7627            }
7628            self.advance();
7629            let reverse = matches!(
7630                self.peek(),
7631                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7632            );
7633            if reverse {
7634                self.advance();
7635            }
7636            let start = self.parse_expr(0)?;
7637            if !matches!(self.peek(), Token::DotDot) {
7638                return Err(self.err(alloc::format!(
7639                    "expected '..' between FOR loop bounds, got {:?}",
7640                    self.peek()
7641                )));
7642            }
7643            self.advance();
7644            let end = self.parse_expr(0)?;
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 LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7649                )));
7650            }
7651            let body = self.parse_plpgsql_stmt_list_until_end()?;
7652            let end_kw = self.expect_ident_like()?;
7653            if !end_kw.eq_ignore_ascii_case("end") {
7654                return Err(self.err(alloc::format!(
7655                    "expected END LOOP after FOR body, got {end_kw:?}"
7656                )));
7657            }
7658            let loop_kw2 = self.expect_ident_like()?;
7659            if !loop_kw2.eq_ignore_ascii_case("loop") {
7660                return Err(self.err(alloc::format!(
7661                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7662                )));
7663            }
7664            return Ok(PlPgSqlStmt::ForRange {
7665                var,
7666                start,
7667                end,
7668                reverse,
7669                body,
7670            });
7671        }
7672        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7673        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7674        {
7675            self.advance();
7676            let body = self.parse_plpgsql_stmt_list_until_end()?;
7677            let end_kw = self.expect_ident_like()?;
7678            if !end_kw.eq_ignore_ascii_case("end") {
7679                return Err(self.err(alloc::format!(
7680                    "expected END LOOP after LOOP body, got {end_kw:?}"
7681                )));
7682            }
7683            let loop_kw = self.expect_ident_like()?;
7684            if !loop_kw.eq_ignore_ascii_case("loop") {
7685                return Err(self.err(alloc::format!(
7686                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7687                )));
7688            }
7689            return Ok(PlPgSqlStmt::Loop { body });
7690        }
7691        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7692        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7693        {
7694            self.advance();
7695            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7696            {
7697                self.advance();
7698                Some(self.parse_expr(0)?)
7699            } else {
7700                None
7701            };
7702            return Ok(PlPgSqlStmt::Exit { when });
7703        }
7704        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7705        // already-parsed Statement or a runtime-computed SQL string.
7706        // The disambiguator vs the extended-query-protocol `EXECUTE
7707        // <stmt_name>` (which is a top-level Statement, not a
7708        // plpgsql line) is that inside a DO block / trigger body the
7709        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7710        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7711        {
7712            self.advance();
7713            let sql = self.parse_expr(0)?;
7714            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7715        }
7716        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7717        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7718        {
7719            self.advance();
7720            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7721            {
7722                self.advance();
7723                Some(self.parse_expr(0)?)
7724            } else {
7725                None
7726            };
7727            return Ok(PlPgSqlStmt::Continue { when });
7728        }
7729        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7730        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7731        {
7732            self.advance();
7733            let condition = self.parse_expr(0)?;
7734            let loop_kw = self.expect_ident_like()?;
7735            if !loop_kw.eq_ignore_ascii_case("loop") {
7736                return Err(self.err(alloc::format!(
7737                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7738                )));
7739            }
7740            let body = self.parse_plpgsql_stmt_list_until_end()?;
7741            // Expect END LOOP.
7742            let end_kw = self.expect_ident_like()?;
7743            if !end_kw.eq_ignore_ascii_case("end") {
7744                return Err(self.err(alloc::format!(
7745                    "expected END LOOP after WHILE body, got {end_kw:?}"
7746                )));
7747            }
7748            let loop_kw2 = self.expect_ident_like()?;
7749            if !loop_kw2.eq_ignore_ascii_case("loop") {
7750                return Err(self.err(alloc::format!(
7751                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7752                )));
7753            }
7754            return Ok(PlPgSqlStmt::While { condition, body });
7755        }
7756        // v7.12.6 — RAISE.
7757        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7758        {
7759            self.advance();
7760            return self.parse_plpgsql_raise();
7761        }
7762        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7763        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7764        {
7765            self.advance();
7766            let condition = self.parse_expr(0)?;
7767            let message = if matches!(self.peek(), Token::Comma) {
7768                self.advance();
7769                Some(self.parse_expr(0)?)
7770            } else {
7771                None
7772            };
7773            return Ok(PlPgSqlStmt::Assert { condition, message });
7774        }
7775        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7776        //   "PERFORM is equivalent to SELECT but discards the
7777        //    result." Side effects (function calls, RAISE inside
7778        //    SQL functions, etc.) still execute. We desugar to
7779        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7780        //    existing embedded-statement path handles execution +
7781        //    result-discard cleanly. The result is naturally
7782        //    discarded because EmbeddedSql doesn't propagate row
7783        //    sets back to the plpgsql interpreter.
7784        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7785        {
7786            self.advance();
7787            // Splice a synthetic Token::Select into the stream at
7788            // the current position so parse_select_stmt parses the
7789            // remainder as a normal SELECT body. Token-stream
7790            // surgery mirrors the try_parse_plpgsql_select_into
7791            // pattern used for SELECT … INTO desugaring.
7792            self.tokens.insert(self.pos, Token::Select);
7793            let select = self.parse_select_stmt()?;
7794            let Statement::Select(s) = select else {
7795                return Err(self.err(alloc::format!(
7796                    "expected SELECT body after PERFORM, got {:?}",
7797                    self.peek()
7798                )));
7799            };
7800            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7801        }
7802        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7803        // plpgsql-specific shape (mailrs round-10 migrate-042).
7804        // PG's SELECT INTO at top-level SQL would CREATE a new
7805        // table; inside plpgsql it ASSIGNS the query result to
7806        // a local variable. We detect the INTO at paren-depth
7807        // 0 between SELECT and the statement boundary; if
7808        // found, split the token stream into "pre-INTO
7809        // projection" + "var" + "post-INTO FROM/WHERE…" and
7810        // rebuild as a SelectInto with a regular SELECT body
7811        // (no INTO clause).
7812        if matches!(self.peek(), Token::Select)
7813            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7814        {
7815            return Ok(PlPgSqlStmt::SelectInto {
7816                var: var_name,
7817                body: Box::new(select_body),
7818            });
7819        }
7820        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7821        // SELECT can appear directly inside a trigger body; we
7822        // recurse into the regular Statement parser, which will
7823        // stop at the trailing `;` (which our caller then
7824        // consumes).
7825        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7826        // also embed ALTER / CREATE / DROP statements; route
7827        // those through the same parser so the DO body parses
7828        // cleanly.
7829        if matches!(self.peek(), Token::Insert)
7830            || matches!(self.peek(), Token::Select)
7831            || matches!(self.peek(), Token::Create)
7832            || matches!(self.peek(), Token::Drop)
7833            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7834                if s.eq_ignore_ascii_case("update")
7835                    || s.eq_ignore_ascii_case("delete")
7836                    || s.eq_ignore_ascii_case("alter"))
7837        {
7838            let stmt = self.parse_one_statement()?;
7839            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7840        }
7841        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7842        // followed by `:=` and an expression.
7843        let target = self.parse_plpgsql_assign_target()?;
7844        // PL/pgSQL assignment uses `:=`. The lexer represents
7845        // this as a colon followed by `=`; check both shapes.
7846        match self.peek() {
7847            Token::ColonEq => {
7848                self.advance();
7849            }
7850            Token::Colon => {
7851                self.advance();
7852                if !matches!(self.peek(), Token::Eq) {
7853                    return Err(self.err(alloc::format!(
7854                        "expected := after plpgsql assign target, got `:` then {:?}",
7855                        self.peek()
7856                    )));
7857                }
7858                self.advance();
7859            }
7860            other => {
7861                return Err(self.err(alloc::format!(
7862                    "expected := after plpgsql assign target, got {other:?}"
7863                )));
7864            }
7865        }
7866        let value = self.parse_expr(0)?;
7867        Ok(PlPgSqlStmt::Assign { target, value })
7868    }
7869
7870    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7871    /// [ELSE body] END IF`. `IF` keyword already consumed.
7872    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7873        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7874        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7875        loop {
7876            // <expr> THEN
7877            let cond = self.parse_expr(0)?;
7878            let then_kw = self.expect_ident_like()?;
7879            if !then_kw.eq_ignore_ascii_case("then") {
7880                return Err(self.err(alloc::format!(
7881                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7882                )));
7883            }
7884            let body = self.parse_plpgsql_stmt_list_until_end()?;
7885            branches.push((cond, body));
7886            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7887            match self.peek() {
7888                Token::Ident(s) | Token::QuotedIdent(s)
7889                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7890                {
7891                    self.advance();
7892                    continue;
7893                }
7894                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7895                    self.advance();
7896                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7897                    break;
7898                }
7899                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7900                    break;
7901                }
7902                other => {
7903                    return Err(self.err(alloc::format!(
7904                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7905                    )));
7906                }
7907            }
7908        }
7909        // Expect `END IF` (the END keyword is the one we're
7910        // looking at right now).
7911        let end_kw = self.expect_ident_like()?;
7912        if !end_kw.eq_ignore_ascii_case("end") {
7913            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7914        }
7915        let if_kw = self.expect_ident_like()?;
7916        if !if_kw.eq_ignore_ascii_case("if") {
7917            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7918        }
7919        Ok(PlPgSqlStmt::If {
7920            branches,
7921            else_branch,
7922        })
7923    }
7924
7925    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7926    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7927    /// is already consumed.
7928    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7929        let lvl_ident = self.expect_ident_like()?;
7930        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7931            "notice" => RaiseLevel::Notice,
7932            "warning" => RaiseLevel::Warning,
7933            "info" => RaiseLevel::Info,
7934            "log" => RaiseLevel::Log,
7935            "debug" => RaiseLevel::Debug,
7936            "exception" => RaiseLevel::Exception,
7937            other => {
7938                return Err(self.err(alloc::format!(
7939                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7940                )));
7941            }
7942        };
7943        // Message: required for v7.12.6. PG accepts a bare
7944        // RAISE-rethrow form (no message), reserved for future
7945        // RAISE-no-args support.
7946        let Token::String(msg) = self.peek() else {
7947            return Err(self.err(alloc::format!(
7948                "expected RAISE message string, got {:?}",
7949                self.peek()
7950            )));
7951        };
7952        let message = msg.clone();
7953        self.advance();
7954        // Optional comma-separated args (PG `%` format substitution).
7955        let mut args: Vec<Expr> = Vec::new();
7956        while matches!(self.peek(), Token::Comma) {
7957            self.advance();
7958            args.push(self.parse_expr(0)?);
7959        }
7960        Ok(PlPgSqlStmt::Raise {
7961            level,
7962            message,
7963            args,
7964        })
7965    }
7966
7967    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7968    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7969    /// migrate-042). Returns `(rebuilt_select_without_into,
7970    /// var_name)` when the pattern matches; `None` for
7971    /// regular SELECTs (those go through the embedded-SQL
7972    /// path). Token-stream surgery so the rebuilt SELECT
7973    /// parses through the regular `parse_select_stmt`.
7974    #[allow(clippy::too_many_lines)]
7975    fn try_parse_plpgsql_select_into(
7976        &mut self,
7977    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
7978        // Scan forward from `self.pos + 1` (past Token::Select)
7979        // for Token::Into at paren-depth 0, stopping at the
7980        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
7981        // end the plpgsql statement.
7982        let start = self.pos;
7983        let mut into_pos: Option<usize> = None;
7984        let mut depth: i32 = 0;
7985        let mut i = start + 1;
7986        while i < self.tokens.len() {
7987            match &self.tokens[i] {
7988                Token::LParen => depth += 1,
7989                Token::RParen => depth -= 1,
7990                Token::Semicolon if depth == 0 => break,
7991                Token::Ident(s)
7992                    if depth == 0
7993                        && (s.eq_ignore_ascii_case("end")
7994                            || s.eq_ignore_ascii_case("else")
7995                            || s.eq_ignore_ascii_case("elsif")) =>
7996                {
7997                    break;
7998                }
7999                Token::Into if depth == 0 => {
8000                    into_pos = Some(i);
8001                    break;
8002                }
8003                _ => {}
8004            }
8005            i += 1;
8006        }
8007        let Some(into_at) = into_pos else {
8008            return Ok(None);
8009        };
8010        // The token immediately after INTO must be the target
8011        // var ident; anything else (e.g. INSERT INTO table)
8012        // ruled out by the depth-0 check above. Capture it.
8013        let var = match self.tokens.get(into_at + 1) {
8014            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
8015            other => {
8016                return Err(self.err(alloc::format!(
8017                    "expected variable name after SELECT … INTO, got {other:?}"
8018                )));
8019            }
8020        };
8021        // Find the end of the plpgsql SELECT INTO statement —
8022        // same boundary rules as the depth-0 scan above.
8023        let mut end = into_at + 2;
8024        let mut depth2: i32 = 0;
8025        while end < self.tokens.len() {
8026            match &self.tokens[end] {
8027                Token::LParen => depth2 += 1,
8028                Token::RParen => depth2 -= 1,
8029                Token::Semicolon if depth2 == 0 => break,
8030                Token::Ident(s)
8031                    if depth2 == 0
8032                        && (s.eq_ignore_ascii_case("end")
8033                            || s.eq_ignore_ascii_case("else")
8034                            || s.eq_ignore_ascii_case("elsif")) =>
8035                {
8036                    break;
8037                }
8038                _ => {}
8039            }
8040            end += 1;
8041        }
8042        // Rebuild a token stream that represents the SELECT
8043        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
8044        // post-var tokens up to statement end]. Run the
8045        // regular `parse_select_stmt` against it.
8046        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
8047        for j in start..into_at {
8048            rebuilt.push(self.tokens[j].clone());
8049        }
8050        for j in (into_at + 2)..end {
8051            rebuilt.push(self.tokens[j].clone());
8052        }
8053        rebuilt.push(Token::Eof);
8054        let saved_pos = self.pos;
8055        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
8056        self.pos = 0;
8057        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
8058        if !matches!(self.peek(), Token::Select) {
8059            self.tokens = saved_tokens;
8060            self.pos = saved_pos;
8061            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
8062        }
8063        let sel = self.parse_select_stmt();
8064        self.tokens = saved_tokens;
8065        self.pos = end;
8066        let sel = sel?;
8067        let Statement::Select(body) = sel else {
8068            return Err(self.err(alloc::format!(
8069                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
8070            )));
8071        };
8072        Ok(Some((body, var)))
8073    }
8074
8075    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8076        // v7.16.1 — read the head token DIRECTLY rather than
8077        // via `expect_ident_like`. The v7.14.0 schema-qualifier
8078        // strip (`public.t` → `t`) inside `expect_ident_like`
8079        // greedily consumes any `ident . ident` pair, which
8080        // silently turned every `NEW.col := …` /
8081        // `OLD.col := …` plpgsql assignment into a Local("col")
8082        // assignment — the head "new"/"old" was eaten as if it
8083        // were a schema name and the Dot was consumed too, so
8084        // this function's own `peek() == Token::Dot` check
8085        // below never fired. Every BEFORE trigger that rewrote
8086        // a NEW cell was a silent no-op for two major releases
8087        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8088        // gate failures were investigated as v7.16.1 backlog.
8089        let head = match self.advance() {
8090            Token::Ident(s) | Token::QuotedIdent(s) => s,
8091            other => {
8092                return Err(self.err(alloc::format!(
8093                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8094                )));
8095            }
8096        };
8097        if matches!(self.peek(), Token::Dot) {
8098            self.advance();
8099            let col = self.expect_ident_like()?;
8100            if head.eq_ignore_ascii_case("new") {
8101                return Ok(AssignTarget::NewColumn(col));
8102            }
8103            if head.eq_ignore_ascii_case("old") {
8104                return Ok(AssignTarget::OldColumn(col));
8105            }
8106            return Err(self.err(alloc::format!(
8107                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8108                 got {head:?}.<col>"
8109            )));
8110        }
8111        Ok(AssignTarget::Local(head))
8112    }
8113
8114    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8115        // RETURN NEW / OLD / NULL — bare-ident forms.
8116        match self.peek() {
8117            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8118                self.advance();
8119                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8120            }
8121            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8122                self.advance();
8123                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8124            }
8125            Token::Null => {
8126                self.advance();
8127                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8128            }
8129            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8130            // per PL/pgSQL convention.
8131            Token::Semicolon => {
8132                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8133            }
8134            _ => {}
8135        }
8136        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8137        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8138        // caller-visible effect (blocks don't return sets), so we
8139        // desugar it identically to PERFORM: parse the SELECT (or
8140        // EXECUTE dynamic) as embedded SQL that runs for side
8141        // effects and discards the result. RETURN NEXT <expr>
8142        // (single-row accumulator) queues with v7.40 SETOF function
8143        // infrastructure.
8144        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8145        // and keep going.
8146        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8147        {
8148            self.advance();
8149            let e = self.parse_expr(0)?;
8150            return Ok(PlPgSqlStmt::ReturnNext(e));
8151        }
8152        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8153        {
8154            self.advance();
8155            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8156            // rows go to the set, like the static form. It used to desugar to a
8157            // bare ExecuteDynamic, whose result was DISCARDED.
8158            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8159            {
8160                self.advance();
8161                let sql = self.parse_expr(0)?;
8162                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8163            }
8164            // Bare RETURN QUERY <select>. If the current token is
8165            // not already SELECT (e.g., the user wrote `RETURN QUERY
8166            // <projection> FROM ...` in a shorthand — rare but PG
8167            // accepts a bare projection here), splice one in. Same
8168            // trick as PERFORM.
8169            if !matches!(self.peek(), Token::Select) {
8170                self.tokens.insert(self.pos, Token::Select);
8171            }
8172            let select = self.parse_select_stmt()?;
8173            let Statement::Select(s) = select else {
8174                return Err(self.err(alloc::format!(
8175                    "expected SELECT body after RETURN QUERY, got {:?}",
8176                    self.peek()
8177                )));
8178            };
8179            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8180            // to an embedded side-effect SELECT whose rows were DISCARDED, which
8181            // in a SETOF function is the entire answer thrown away.
8182            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8183        }
8184        // Fall through: parse a full expression.
8185        let e = self.parse_expr(0)?;
8186        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8187    }
8188
8189    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8190        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8191        // are ident-shaped (the parser keys off case-insensitive
8192        // match — same shape used by the top-level Update / Delete
8193        // dispatchers at parse_one_statement).
8194        if matches!(self.peek(), Token::Insert) {
8195            self.advance();
8196            return Ok(TriggerEvent::Insert);
8197        }
8198        match self.peek() {
8199            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8200                self.advance();
8201                Ok(TriggerEvent::Update)
8202            }
8203            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8204                self.advance();
8205                Ok(TriggerEvent::Delete)
8206            }
8207            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8208                self.advance();
8209                Ok(TriggerEvent::Truncate)
8210            }
8211            other => Err(self.err(alloc::format!(
8212                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8213            ))),
8214        }
8215    }
8216
8217    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8218    ///   - (no clause) → implicit `FOR ALL TABLES`
8219    ///   - `FOR ALL TABLES`
8220    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8221    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8222    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
8223    ///     REJECTS the bare plural (`invalid publication object list`,
8224    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
8225    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8226    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8227        let name = self.expect_ident_or_string()?;
8228        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8229        // shape so existing publications keep parsing identically.
8230        let scope = if matches!(self.peek(), Token::For) {
8231            self.advance();
8232            if matches!(self.peek(), Token::All) {
8233                self.advance();
8234                if !matches!(self.peek(), Token::Tables) {
8235                    return Err(self.err(format!(
8236                        "expected TABLES after FOR ALL, got {:?}",
8237                        self.peek()
8238                    )));
8239                }
8240                self.advance();
8241                if matches!(self.peek(), Token::Except) {
8242                    self.advance();
8243                    let tables = self.parse_publication_table_list()?;
8244                    PublicationScope::AllTablesExcept(tables)
8245                } else {
8246                    PublicationScope::AllTables
8247                }
8248            } else if matches!(self.peek(), Token::Table) {
8249                self.advance();
8250                let tables = self.parse_publication_table_list()?;
8251                PublicationScope::ForTables(tables)
8252            } else if matches!(self.peek(), Token::Tables) {
8253                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8254                // plural (`FOR TABLES t`) is REJECTED (`invalid
8255                // publication object list`); TABLES only pairs with
8256                // `IN SCHEMA`. The old arm accepted it on an
8257                // unverifiable "PG 19 accepts both" claim.
8258                self.advance();
8259                if !matches!(self.peek(), Token::In) {
8260                    return Err(self.err(alloc::string::String::from(
8261                        "invalid publication object list",
8262                    )));
8263                }
8264                self.advance();
8265                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8266                    return Err(self.err(format!(
8267                        "expected SCHEMA after FOR TABLES IN, got {:?}",
8268                        self.peek()
8269                    )));
8270                }
8271                self.advance();
8272                let schema = self.expect_ident_or_string()?;
8273                PublicationScope::TablesInSchema(schema)
8274            } else {
8275                return Err(self.err(format!(
8276                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8277                    self.peek()
8278                )));
8279            }
8280        } else {
8281            PublicationScope::AllTables
8282        };
8283        Ok(Statement::CreatePublication(CreatePublicationStatement {
8284            name,
8285            scope,
8286        }))
8287    }
8288
8289    /// v6.1.3 — Comma-separated identifier list for the publication
8290    /// FOR-clause. Requires at least one entry; empty list is a
8291    /// parse error (PG behaviour). Quoted idents are accepted; the
8292    /// names round-trip through `Display` as `quote_ident(name)`.
8293    ///
8294    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8295    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8296    /// pg_dump output. SPG's publication state today is per-table
8297    /// only (matching the pre-PG-15 surface); the col list + WHERE
8298    /// are parsed so dumps load through and the table name reaches
8299    /// `PublicationScope::ForTables`, but the filter is not enforced
8300    /// at publish time. Re-open when a customer dogfood gate
8301    /// requires per-row-filter or column-subset publish semantics
8302    /// (which gates on persistent slot state landing first, 21.12).
8303    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8304        let first = self.parse_publication_table_entry()?;
8305        let mut out = alloc::vec![first];
8306        while matches!(self.peek(), Token::Comma) {
8307            self.advance();
8308            out.push(self.parse_publication_table_entry()?);
8309        }
8310        Ok(out)
8311    }
8312
8313    /// One table entry inside a FOR TABLE clause:
8314    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8315    /// Returns just the table name; the column list + WHERE predicate
8316    /// are consumed and discarded per the parse-accept-discard
8317    /// commitment above.
8318    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8319        let name = self.expect_ident_like()?;
8320        // Optional column list — `(col, col, …)`.
8321        if matches!(self.peek(), Token::LParen) {
8322            self.advance();
8323            // Empty parens are a PG error too; require ≥ 1 column.
8324            let _ = self.expect_ident_like()?;
8325            while matches!(self.peek(), Token::Comma) {
8326                self.advance();
8327                let _ = self.expect_ident_like()?;
8328            }
8329            if !matches!(self.peek(), Token::RParen) {
8330                return Err(self.err(alloc::format!(
8331                    "expected ')' to close publication column list, got {:?}",
8332                    self.peek()
8333                )));
8334            }
8335            self.advance();
8336        }
8337        // Optional row filter — `WHERE (predicate)`.
8338        if matches!(self.peek(), Token::Where) {
8339            self.advance();
8340            if !matches!(self.peek(), Token::LParen) {
8341                return Err(self.err(alloc::format!(
8342                    "expected '(' after WHERE in publication row filter, got {:?}",
8343                    self.peek()
8344                )));
8345            }
8346            self.advance();
8347            let _ = self.parse_expr(0)?;
8348            if !matches!(self.peek(), Token::RParen) {
8349                return Err(self.err(alloc::format!(
8350                    "expected ')' to close publication WHERE filter, got {:?}",
8351                    self.peek()
8352                )));
8353            }
8354            self.advance();
8355        }
8356        Ok(name)
8357    }
8358
8359    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8360    ///                 CONNECTION '<conn>'
8361    ///                 PUBLICATION <pub> [, <pub> ...]`.
8362    ///
8363    /// The clause order is fixed (CONNECTION first, then
8364    /// PUBLICATION) to match PG. No WITH-options accepted in
8365    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8366    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8367        let name = self.expect_ident_or_string()?;
8368        if !matches!(self.peek(), Token::Connection) {
8369            return Err(self.err(format!(
8370                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8371                self.peek()
8372            )));
8373        }
8374        self.advance();
8375        let conn_str = self.expect_string_literal()?;
8376        if !matches!(self.peek(), Token::Publication) {
8377            return Err(self.err(format!(
8378                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8379                self.peek()
8380            )));
8381        }
8382        self.advance();
8383        // Reuse the publication FOR-list parser shape: at least one
8384        // identifier, comma-separated.
8385        let first = self.expect_ident_like()?;
8386        let mut publications = alloc::vec![first];
8387        while matches!(self.peek(), Token::Comma) {
8388            self.advance();
8389            publications.push(self.expect_ident_like()?);
8390        }
8391        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8392            name,
8393            conn_str,
8394            publications,
8395        }))
8396    }
8397
8398    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8399    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8400    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8401    /// that fit `u64`.
8402    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8403    /// qualifier is a *namespace* the app owns (`app.user_id`,
8404    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8405    /// to discard. So parse the raw segments here instead of
8406    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8407    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8408    /// a single segment and round-trip unchanged.
8409    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8410        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8411        loop {
8412            let seg = match self.advance() {
8413                Token::Ident(s) | Token::QuotedIdent(s) => s,
8414                other if unreserved_keyword_text(&other).is_some() => {
8415                    unreserved_keyword_text(&other).unwrap()
8416                }
8417                other => {
8418                    return Err(ParseError {
8419                        message: format!("expected parameter name, got {other:?}"),
8420                        token_pos: self.consumed_pos(),
8421                    });
8422                }
8423            };
8424            parts.push(seg);
8425            if matches!(self.peek(), Token::Dot) {
8426                self.advance();
8427                continue;
8428            }
8429            break;
8430        }
8431        Ok(parts.join(".").to_ascii_lowercase())
8432    }
8433
8434    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8435        Self::parse_set_value_inner(self)
8436    }
8437
8438    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8439        match self.advance() {
8440            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8441            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8442                Ok(crate::ast::SetValue::Default)
8443            }
8444            Token::Ident(s) | Token::QuotedIdent(s) => {
8445                let mut accum = s;
8446                while matches!(self.peek(), Token::Dot) {
8447                    self.advance();
8448                    let next = self.expect_ident_like()?;
8449                    accum.push('.');
8450                    accum.push_str(&next);
8451                }
8452                Ok(crate::ast::SetValue::Ident(accum))
8453            }
8454            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8455            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8456            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8457            // spellings that lex as keyword tokens, not idents:
8458            // `SET standard_conforming_strings = on` is in every
8459            // pg_dump preamble (`off` already lexes as an ident).
8460            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8461            // DEFAULT lexes as its keyword token, so the ident arm above
8462            // never saw it and the everyday reset form was a syntax error.
8463            Token::Default => Ok(crate::ast::SetValue::Default),
8464            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8465            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8466            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8467            // v7.14.0 — MySQL session/user variable RHS
8468            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8469            // Wrap as Ident so the SET handler can record it; the
8470            // engine treats `@VAR` / `@@VAR` values as opaque
8471            // strings.
8472            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8473            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8474            // is the common MySQL preamble shape. Allow a `+` or
8475            // `-` prefix on negative numerics for parity with PG
8476            // (some param defaults are negative).
8477            Token::Minus => match self.advance() {
8478                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8479                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8480                other => Err(self.err(format!(
8481                    "expected numeric after `-` in SET value, got {other:?}"
8482                ))),
8483            },
8484            other => Err(self.err(format!(
8485                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8486            ))),
8487        }
8488    }
8489
8490    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8491    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8492    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8493    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8494    /// present). Modes are comma-separated per PG; SPG also
8495    /// accepts space-separated for tolerance. READ ONLY / WRITE
8496    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8497    /// surface but not behaviorally honoured today).
8498    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8499    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8500    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8501    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8502    /// session default rather than forcing READ COMMITTED.
8503    fn parse_isolation_level_clauses(
8504        &mut self,
8505    ) -> Result<crate::ast::TransactionModes, ParseError> {
8506        let mut level = IsolationLevel::default();
8507        let mut have_level = false;
8508        // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8509        // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8510        let mut read_only: Option<bool> = None;
8511        loop {
8512            // ISOLATION LEVEL …
8513            let saw_isolation =
8514                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8515            if saw_isolation {
8516                self.advance(); // ISOLATION
8517                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8518                    return Err(self.err(alloc::format!(
8519                        "expected LEVEL after ISOLATION, got {:?}",
8520                        self.peek()
8521                    )));
8522                }
8523                self.advance(); // LEVEL
8524                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8525                let w1 = self
8526                    .expect_ident_like()
8527                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8528                let lc = w1.to_ascii_lowercase();
8529                level = match lc.as_str() {
8530                    "serializable" => IsolationLevel::Serializable,
8531                    "repeatable" => {
8532                        // Expect READ
8533                        let w2 = self
8534                            .expect_ident_like()
8535                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8536                        if !w2.eq_ignore_ascii_case("read") {
8537                            return Err(self.err(alloc::format!(
8538                                "expected READ after REPEATABLE, got {w2:?}"
8539                            )));
8540                        }
8541                        IsolationLevel::RepeatableRead
8542                    }
8543                    "read" => {
8544                        let w2 = self
8545                            .expect_ident_like()
8546                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8547                        match w2.to_ascii_lowercase().as_str() {
8548                            "committed" => IsolationLevel::ReadCommitted,
8549                            "uncommitted" => IsolationLevel::ReadUncommitted,
8550                            other => {
8551                                return Err(self.err(alloc::format!(
8552                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8553                                )));
8554                            }
8555                        }
8556                    }
8557                    other => {
8558                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8559                    }
8560                };
8561                have_level = true;
8562            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8563                // v7.39 — READ ONLY | READ WRITE. The comment here used to
8564                // read "parsed, not behaviorally honoured", and it was
8565                // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8566                // opened an ordinary read-write transaction and accepted
8567                // every write in it.
8568                self.advance();
8569                match self.peek().clone() {
8570                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8571                        self.advance();
8572                        read_only = Some(true);
8573                    }
8574                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8575                        self.advance();
8576                        read_only = Some(false);
8577                    }
8578                    other => {
8579                        return Err(self.err(alloc::format!(
8580                            "expected ONLY or WRITE after READ, got {other:?}"
8581                        )));
8582                    }
8583                }
8584            } else if matches!(self.peek(), Token::Not) {
8585                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8586                self.advance();
8587                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8588                    return Err(self.err(alloc::format!(
8589                        "expected DEFERRABLE after NOT, got {:?}",
8590                        self.peek()
8591                    )));
8592                }
8593                self.advance();
8594            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8595            {
8596                self.advance();
8597            } else {
8598                break;
8599            }
8600            // Optional comma between modes.
8601            if matches!(self.peek(), Token::Comma) {
8602                self.advance();
8603            }
8604        }
8605        Ok(crate::ast::TransactionModes {
8606            isolation: have_level.then_some(level),
8607            read_only,
8608        })
8609    }
8610
8611    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8612        // FOR is a v6.1.2-reserved keyword (Token::For). The
8613        // other two are bare idents — they've never needed lexer
8614        // support and we keep it that way.
8615        if !matches!(self.peek(), Token::For) {
8616            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8617        }
8618        self.advance();
8619        self.expect_keyword_ident("wal")?;
8620        self.expect_keyword_ident("position")?;
8621        let pos = self.expect_u64_literal()?;
8622        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8623        {
8624            self.advance();
8625            self.expect_keyword_ident("timeout")?;
8626            Some(self.expect_u64_literal()?)
8627        } else {
8628            None
8629        };
8630        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8631    }
8632
8633    /// v6.1.7 helper — consume a `Token::Integer` and check it
8634    /// fits `u64`. WAL positions and millisecond timeouts are
8635    /// non-negative.
8636    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8637        match self.advance() {
8638            Token::Integer(n) if n >= 0 => Ok(n as u64),
8639            Token::Integer(n) => Err(ParseError {
8640                message: format!("expected non-negative integer, got {n}"),
8641                token_pos: self.consumed_pos(),
8642            }),
8643            other => Err(ParseError {
8644                message: format!("expected integer literal, got {other:?}"),
8645                token_pos: self.consumed_pos(),
8646            }),
8647        }
8648    }
8649
8650    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8651    /// ROLE '<role>' (defaults to readonly). All string slots accept
8652    /// either a quoted ident or a quoted string literal.
8653    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8654    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8655    ///
8656    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8657    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8658    /// wire role) still parses — it is a different axis from the PG attributes.
8659    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8660    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8661    /// or RESET, so the plain attribute forms keep their old path.
8662    fn peeks_db_role_setting(&self) -> bool {
8663        let mut i = self.pos + 1; // past the object's name
8664        let word = |p: usize| -> Option<String> {
8665            match self.tokens.get(p) {
8666                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8667                Some(Token::In) => Some(String::from("in")),
8668                _ => None,
8669            }
8670        };
8671        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8672            i += 3; // IN DATABASE <name>
8673        }
8674        matches!(word(i).as_deref(), Some("set" | "reset"))
8675    }
8676
8677    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8678        use crate::ast::SetDbRoleSettingStatement;
8679        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8680        // identifier, so the ordinary name reader refuses it. Same trap
8681        // as TABLE / INDEX / FULL / DEFAULT before it.
8682        let name = if matches!(self.peek(), Token::All) {
8683            self.advance();
8684            String::from("all")
8685        } else {
8686            self.expect_ident_or_string()?
8687        };
8688        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8689        let all = name.eq_ignore_ascii_case("all");
8690        let (mut database, mut role) = if is_database {
8691            (Some(name), None)
8692        } else if all {
8693            (None, None)
8694        } else {
8695            (None, Some(name))
8696        };
8697        if matches!(self.peek(), Token::In) {
8698            self.advance();
8699            self.advance(); // DATABASE
8700            database = Some(self.expect_ident_or_string()?);
8701        }
8702        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8703        self.advance(); // SET | RESET
8704        if resetting && matches!(self.peek(), Token::All) {
8705            self.advance();
8706            self.consume_until_statement_boundary();
8707            return Ok(Statement::SetDbRoleSetting(Box::new(
8708                SetDbRoleSettingStatement {
8709                    database,
8710                    role,
8711                    param: None,
8712                    value: None,
8713                },
8714            )));
8715        }
8716        let param = self.expect_ident_like()?;
8717        let value = if resetting {
8718            None
8719        } else {
8720            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8721            // KEYWORD, so the ident-only check missed it and consumed
8722            // the word itself as the value — the same trap as ALL, one
8723            // clause over.
8724            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8725                self.advance();
8726            }
8727            Some(self.take_guc_value())
8728        };
8729        self.consume_until_statement_boundary();
8730        Ok(Statement::SetDbRoleSetting(Box::new(
8731            SetDbRoleSettingStatement {
8732                database,
8733                role,
8734                param: Some(param),
8735                value,
8736            },
8737        )))
8738    }
8739
8740    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8741    /// a quoted literal loses its quotes, a bare word or number does not.
8742    fn take_guc_value(&mut self) -> String {
8743        match self.advance() {
8744            Token::String(s) => s,
8745            Token::Integer(n) => format!("{n}"),
8746            Token::Float(f) => format!("{f}"),
8747            Token::Ident(s) | Token::QuotedIdent(s) => s,
8748            other => format!("{other:?}"),
8749        }
8750    }
8751
8752    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8753        let name = self.expect_ident_or_string()?;
8754        if self.peek_keyword_ident("with") {
8755            self.advance();
8756        }
8757        let mut password = String::new();
8758        let mut role = String::new();
8759        let mut login: Option<bool> = None;
8760        let mut inherit: Option<bool> = None;
8761        let mut superuser: Option<bool> = None;
8762        // Not a `while let`: the pattern would borrow `self` across the
8763        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8764        #[allow(clippy::while_let_loop)]
8765        loop {
8766            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8767                break;
8768            };
8769            match w.to_ascii_lowercase().as_str() {
8770                "password" => {
8771                    self.advance();
8772                    password = self.expect_string_literal()?;
8773                }
8774                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8775                // is the same slot.
8776                "encrypted" => {
8777                    self.advance();
8778                    self.expect_keyword_ident("password")?;
8779                    password = self.expect_string_literal()?;
8780                }
8781                "login" => {
8782                    self.advance();
8783                    login = Some(true);
8784                }
8785                "nologin" => {
8786                    self.advance();
8787                    login = Some(false);
8788                }
8789                "inherit" => {
8790                    self.advance();
8791                    inherit = Some(true);
8792                }
8793                "noinherit" => {
8794                    self.advance();
8795                    inherit = Some(false);
8796                }
8797                "superuser" => {
8798                    self.advance();
8799                    superuser = Some(true);
8800                }
8801                "nosuperuser" => {
8802                    self.advance();
8803                    superuser = Some(false);
8804                }
8805                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8806                "role" => {
8807                    self.advance();
8808                    role = self.expect_string_literal()?;
8809                }
8810                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8811                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8812                // accepted and ignored so a pg_dump role block restores. They
8813                // gate capabilities SPG does not have.
8814                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8815                | "noreplication" | "bypassrls" | "nobypassrls" => {
8816                    self.advance();
8817                }
8818                "connection" => {
8819                    self.advance();
8820                    self.expect_keyword_ident("limit")?;
8821                    self.advance(); // the number
8822                }
8823                "valid" => {
8824                    self.advance();
8825                    self.expect_keyword_ident("until")?;
8826                    self.expect_string_literal()?;
8827                }
8828                _ => break,
8829            }
8830        }
8831        if role.is_empty() {
8832            role = "readonly".to_string();
8833        }
8834        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8835            name,
8836            password,
8837            role,
8838            login,
8839            inherit,
8840            superuser,
8841            is_user,
8842        }))
8843    }
8844
8845    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8846    /// consumed the USING / WITH CHECK keyword.
8847    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8848        if !matches!(self.peek(), Token::LParen) {
8849            return Err(self.err(alloc::format!(
8850                "expected '(' after {clause}, got {:?}",
8851                self.peek()
8852            )));
8853        }
8854        self.advance();
8855        let e = self.parse_expr(0)?;
8856        if !matches!(self.peek(), Token::RParen) {
8857            return Err(self.err(alloc::format!(
8858                "expected ')' to close {clause}, got {:?}",
8859                self.peek()
8860            )));
8861        }
8862        self.advance();
8863        Ok(e)
8864    }
8865
8866    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8867    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8868        let mut roles = Vec::new();
8869        loop {
8870            roles.push(self.expect_ident_like()?);
8871            if matches!(self.peek(), Token::Comma) {
8872                self.advance();
8873            } else {
8874                break;
8875            }
8876        }
8877        Ok(roles)
8878    }
8879
8880    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8881    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8882    /// `CREATE POLICY`.
8883    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8884        use crate::ast::PolicyCmd;
8885        let name = self.expect_ident_like()?;
8886        if !matches!(self.peek(), Token::On) {
8887            return Err(self.err(alloc::format!(
8888                "expected ON after CREATE POLICY name, got {:?}",
8889                self.peek()
8890            )));
8891        }
8892        self.advance();
8893        let table = self.expect_ident_like()?;
8894
8895        let mut permissive = true;
8896        if matches!(self.peek(), Token::As) {
8897            self.advance();
8898            let w = self.expect_ident_like()?;
8899            permissive = if w.eq_ignore_ascii_case("permissive") {
8900                true
8901            } else if w.eq_ignore_ascii_case("restrictive") {
8902                false
8903            } else {
8904                return Err(self.err(alloc::format!(
8905                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8906                )));
8907            };
8908        }
8909
8910        let mut cmd = PolicyCmd::All;
8911        if matches!(self.peek(), Token::For) {
8912            self.advance();
8913            cmd = self.parse_policy_cmd()?;
8914        }
8915
8916        let mut roles = Vec::new();
8917        if matches!(self.peek(), Token::To) {
8918            self.advance();
8919            roles = self.parse_policy_roles()?;
8920        }
8921
8922        let mut using = None;
8923        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8924        {
8925            self.advance();
8926            using = Some(self.parse_paren_expr("USING")?);
8927        }
8928
8929        let mut with_check = None;
8930        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8931        {
8932            self.advance();
8933            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8934            {
8935                return Err(self.err(alloc::format!(
8936                    "expected CHECK after WITH, got {:?}",
8937                    self.peek()
8938                )));
8939            }
8940            self.advance();
8941            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8942        }
8943
8944        // Clause-per-command matrix (PG wording).
8945        match cmd {
8946            PolicyCmd::Insert => {
8947                if using.is_some() {
8948                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8949                }
8950            }
8951            PolicyCmd::Select | PolicyCmd::Delete => {
8952                if with_check.is_some() {
8953                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8954                }
8955            }
8956            PolicyCmd::Update | PolicyCmd::All => {}
8957        }
8958
8959        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8960            name,
8961            table,
8962            permissive,
8963            cmd,
8964            roles,
8965            using,
8966            with_check,
8967        }))
8968    }
8969
8970    /// v7.39 (RLS) — the command word after `FOR`.
8971    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
8972        use crate::ast::PolicyCmd;
8973        match self.peek().clone() {
8974            Token::All => {
8975                self.advance();
8976                Ok(PolicyCmd::All)
8977            }
8978            Token::Select => {
8979                self.advance();
8980                Ok(PolicyCmd::Select)
8981            }
8982            Token::Insert => {
8983                self.advance();
8984                Ok(PolicyCmd::Insert)
8985            }
8986            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
8987                self.advance();
8988                Ok(PolicyCmd::Update)
8989            }
8990            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
8991                self.advance();
8992                Ok(PolicyCmd::Delete)
8993            }
8994            other => Err(self.err(alloc::format!(
8995                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
8996            ))),
8997        }
8998    }
8999
9000    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
9001    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
9002    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9003        let name = self.expect_ident_like()?;
9004        if !matches!(self.peek(), Token::On) {
9005            return Err(self.err(alloc::format!(
9006                "expected ON after ALTER POLICY name, got {:?}",
9007                self.peek()
9008            )));
9009        }
9010        self.advance();
9011        let table = self.expect_ident_like()?;
9012
9013        // RENAME TO new
9014        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
9015        {
9016            self.advance();
9017            if !matches!(self.peek(), Token::To) {
9018                return Err(self.err(alloc::format!(
9019                    "expected TO after RENAME, got {:?}",
9020                    self.peek()
9021                )));
9022            }
9023            self.advance();
9024            let new = self.expect_ident_like()?;
9025            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9026                name,
9027                table,
9028                rename_to: Some(new),
9029                roles: None,
9030                using: None,
9031                with_check: None,
9032            }));
9033        }
9034
9035        let mut roles = None;
9036        if matches!(self.peek(), Token::To) {
9037            self.advance();
9038            roles = Some(self.parse_policy_roles()?);
9039        }
9040        let mut using = None;
9041        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9042        {
9043            self.advance();
9044            using = Some(self.parse_paren_expr("USING")?);
9045        }
9046        let mut with_check = None;
9047        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9048        {
9049            self.advance();
9050            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9051            {
9052                return Err(self.err(alloc::format!(
9053                    "expected CHECK after WITH, got {:?}",
9054                    self.peek()
9055                )));
9056            }
9057            self.advance();
9058            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9059        }
9060        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9061            name,
9062            table,
9063            rename_to: None,
9064            roles,
9065            using,
9066            with_check,
9067        }))
9068    }
9069
9070    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
9071    /// `DROP POLICY`.
9072    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9073        let if_exists = self.consume_if_exists();
9074        let name = self.expect_ident_like()?;
9075        if !matches!(self.peek(), Token::On) {
9076            return Err(self.err(alloc::format!(
9077                "expected ON after DROP POLICY name, got {:?}",
9078                self.peek()
9079            )));
9080        }
9081        self.advance();
9082        let table = self.expect_ident_like()?;
9083        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9084            name,
9085            table,
9086            if_exists,
9087        }))
9088    }
9089}
9090fn wrap_from_leaves(
9091    e: &mut Expr,
9092    names: &[String],
9093    make: &dyn Fn(Expr) -> Expr,
9094    refs: &dyn Fn(&Expr) -> bool,
9095) {
9096    if let Expr::Column(c) = e {
9097        if c.qualifier
9098            .as_deref()
9099            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9100        {
9101            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9102            *e = make(taken);
9103        }
9104        return;
9105    }
9106    match e {
9107        Expr::Binary { lhs, rhs, .. } => {
9108            wrap_from_leaves(lhs, names, make, refs);
9109            wrap_from_leaves(rhs, names, make, refs);
9110        }
9111        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9112            wrap_from_leaves(expr, names, make, refs)
9113        }
9114        Expr::FunctionCall { args, .. } => {
9115            for a in args.iter_mut() {
9116                wrap_from_leaves(a, names, make, refs);
9117            }
9118        }
9119        Expr::Case {
9120            operand,
9121            branches,
9122            else_branch,
9123        } => {
9124            if let Some(o) = operand.as_deref_mut() {
9125                wrap_from_leaves(o, names, make, refs);
9126            }
9127            for (w, t) in branches.iter_mut() {
9128                wrap_from_leaves(w, names, make, refs);
9129                wrap_from_leaves(t, names, make, refs);
9130            }
9131            if let Some(el) = else_branch.as_deref_mut() {
9132                wrap_from_leaves(el, names, make, refs);
9133            }
9134        }
9135        // Compound variants the walk doesn't decompose: keep the
9136        // pre-D.30 behavior — wrap the whole sub-expr if it touches
9137        // a source table, so nothing regresses.
9138        other => {
9139            if refs(other) {
9140                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9141                *other = make(taken);
9142            }
9143        }
9144    }
9145}
9146
9147/// v7.39 (round 241) — does this expression reference any of the FROM /
9148/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9149/// lowerings)?
9150fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9151    match e {
9152        Expr::Column(c) => c
9153            .qualifier
9154            .as_deref()
9155            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9156        Expr::Binary { lhs, rhs, .. } => {
9157            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9158        }
9159        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9160        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9161        Expr::Case {
9162            operand,
9163            branches,
9164            else_branch,
9165        } => {
9166            operand
9167                .as_deref()
9168                .is_some_and(|o| expr_refs_tables(o, names))
9169                || branches
9170                    .iter()
9171                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9172                || else_branch
9173                    .as_deref()
9174                    .is_some_and(|el| expr_refs_tables(el, names))
9175        }
9176        _ => false,
9177    }
9178}
9179
9180impl Parser {
9181    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9182    /// Caller already consumed the leading `UPDATE` ident.
9183    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9184    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9185    /// after the target name has been read. `JOIN` is a reserved token;
9186    /// the qualifiers are bare idents.
9187    fn peek_is_update_join_start(&self) -> bool {
9188        match self.peek() {
9189            // JOIN and its qualifiers are reserved lexer tokens (the grammar
9190            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9191            Token::Join
9192            | Token::Inner
9193            | Token::Left
9194            | Token::Right
9195            | Token::Cross
9196            | Token::Full => true,
9197            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9198            Token::Ident(s) | Token::QuotedIdent(s) => {
9199                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9200            }
9201            _ => false,
9202        }
9203    }
9204
9205    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9206    /// USER-variable assignment. Its own per-session namespace, an arbitrary
9207    /// expression on the right, and `:=` as a second spelling of `=`.
9208    ///
9209    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9210    /// from sits on the nesting recursion chain (a CTE body, a subquery),
9211    /// and holding this loop's `Vec` + `String` locals there overflowed the
9212    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9213    #[inline(never)]
9214    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9215        let mut assigns: Vec<(String, Expr)> = Vec::new();
9216        let mut settings: Vec<(String, Expr)> = Vec::new();
9217        loop {
9218            // v7.39 (round 554) — a plain NAME here is a session
9219            // setting, not a user variable. mysqldump writes the two in
9220            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9221            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9222            // changes it — and this refused the mixture outright, so no
9223            // dump could be restored past its preamble.
9224            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9225                self.advance();
9226                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9227                    return Err(self.err(alloc::format!(
9228                        "expected `=` after {name}, got {:?}",
9229                        self.peek()
9230                    )));
9231                }
9232                self.advance();
9233                let value = self.parse_expr(0)?;
9234                settings.push((name.to_ascii_lowercase(), value));
9235                if matches!(self.peek(), Token::Comma) {
9236                    self.advance();
9237                    continue;
9238                }
9239                break;
9240            }
9241            let Token::SessionVar(raw) = self.peek().clone() else {
9242                return Err(self.err(alloc::format!(
9243                    "expected a user variable after SET, got {:?}",
9244                    self.peek()
9245                )));
9246            };
9247            if raw.starts_with("@@") {
9248                return Err(self.err(alloc::string::String::from(
9249                    "cannot mix `@@` settings with `@` user variables in one SET",
9250                )));
9251            }
9252            self.advance();
9253            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9254                return Err(self.err(alloc::format!(
9255                    "expected `=` or `:=` after {raw}, got {:?}",
9256                    self.peek()
9257                )));
9258            }
9259            self.advance();
9260            let value = self.parse_expr(0)?;
9261            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9262            if matches!(self.peek(), Token::Comma) {
9263                self.advance();
9264                continue;
9265            }
9266            break;
9267        }
9268        Ok(Statement::SetUserVars(assigns, settings))
9269    }
9270
9271    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9272        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9273        // NAMED `only` until now, which failed on `relation "only" does
9274        // not exist`. The lookahead is what keeps a table actually
9275        // called `only` working: the keyword is only a keyword when a
9276        // TABLE NAME follows it — and `SET` arrives as an identifier
9277        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9278        // for the table and die on the `=`. Measured by the pin.
9279        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9280            if s.eq_ignore_ascii_case("only"))
9281            && matches!(
9282                self.tokens.get(self.pos + 1),
9283                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9284            );
9285        if only {
9286            self.advance();
9287        }
9288        let table = self.expect_ident_like()?;
9289        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9290        // bare spelling; a bare identifier that is the SET keyword itself
9291        // is the clause, not an alias.
9292        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9293        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9294        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9295        // following JOIN a syntax error.
9296        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9297        let alias = if matches!(self.peek(), Token::As) {
9298            self.advance();
9299            Some(self.expect_ident_like()?)
9300        } else {
9301            match self.peek() {
9302                Token::Ident(s) | Token::QuotedIdent(s)
9303                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
9304                {
9305                    let a = s.clone();
9306                    self.advance();
9307                    Some(a)
9308                }
9309                _ => None,
9310            }
9311        };
9312        // v7.39 (round 420) — MySQL's multi-table UPDATE:
9313        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
9314        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
9315        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9316        // The FIRST table is the mutation target and the rest are sources —
9317        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9318        // SPG already lowers onto correlated subqueries. So rewind, let
9319        // `parse_from_clause` read the whole list (it handles aliases, comma
9320        // lists, and every JOIN form), then peel the target off the front.
9321        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9322            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9323        {
9324            // NOTE: `advance()` destroys the tokens it returns
9325            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9326            // is NOT possible — the tail is read forward, once, through the
9327            // same grammar `parse_from_clause` uses after its primary.
9328            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9329            let mut joins = self.parse_from_joins(&target_qual)?;
9330            if joins.is_empty() {
9331                return Err(self.err(alloc::string::String::from(
9332                    "multi-table UPDATE needs at least one source table",
9333                )));
9334            }
9335            let head = joins.remove(0);
9336            // A LEFT join keeps every target row (the unmatched ones see NULL
9337            // on the source side), so it must NOT get the EXISTS row filter
9338            // the inner / comma forms use.
9339            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9340            let src = FromClause {
9341                primary: head.table,
9342                joins,
9343            };
9344            (Some(src), head.on, outer)
9345        } else {
9346            (None, None, false)
9347        };
9348        self.expect_keyword_ident("set")?;
9349        let mut assignments = Vec::new();
9350        loop {
9351            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9352            // …)` — the parenthesized multi-assignment. Expressions
9353            // assign positionally; a subquery RHS clones per column
9354            // keeping only the Nth projection item.
9355            if matches!(self.peek(), Token::LParen) {
9356                self.advance();
9357                let mut cols = alloc::vec![self.expect_ident_like()?];
9358                while matches!(self.peek(), Token::Comma) {
9359                    self.advance();
9360                    cols.push(self.expect_ident_like()?);
9361                }
9362                if !matches!(self.peek(), Token::RParen) {
9363                    return Err(self.err(format!(
9364                        "expected ')' after SET column list, got {:?}",
9365                        self.peek()
9366                    )));
9367                }
9368                self.advance();
9369                if !matches!(self.peek(), Token::Eq) {
9370                    return Err(self.err(format!(
9371                        "expected `=` after SET column list, got {:?}",
9372                        self.peek()
9373                    )));
9374                }
9375                self.advance();
9376                if !matches!(self.peek(), Token::LParen) {
9377                    return Err(self.err(format!(
9378                        "expected '(' after SET (…) =, got {:?}",
9379                        self.peek()
9380                    )));
9381                }
9382                self.advance();
9383                if matches!(self.peek(), Token::Select) {
9384                    let inner = match self.parse_select_stmt()? {
9385                        Statement::Select(s) => s,
9386                        other => {
9387                            return Err(self.err(alloc::format!(
9388                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9389                            )));
9390                        }
9391                    };
9392                    if !matches!(self.peek(), Token::RParen) {
9393                        return Err(self.err(format!(
9394                            "expected ')' after SET subquery, got {:?}",
9395                            self.peek()
9396                        )));
9397                    }
9398                    self.advance();
9399                    if inner.items.len() != cols.len() {
9400                        return Err(self.err(alloc::format!(
9401                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9402                            cols.len(),
9403                            inner.items.len()
9404                        )));
9405                    }
9406                    for (i, col) in cols.into_iter().enumerate() {
9407                        let mut sub = inner.clone();
9408                        sub.items = alloc::vec![sub.items[i].clone()];
9409                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9410                    }
9411                } else {
9412                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9413                    while matches!(self.peek(), Token::Comma) {
9414                        self.advance();
9415                        exprs.push(self.parse_expr(0)?);
9416                    }
9417                    if !matches!(self.peek(), Token::RParen) {
9418                        return Err(self.err(format!(
9419                            "expected ')' after SET row values, got {:?}",
9420                            self.peek()
9421                        )));
9422                    }
9423                    self.advance();
9424                    if exprs.len() != cols.len() {
9425                        return Err(self.err(alloc::format!(
9426                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9427                            cols.len(),
9428                            exprs.len()
9429                        )));
9430                    }
9431                    for (col, e) in cols.into_iter().zip(exprs) {
9432                        assignments.push((col, e));
9433                    }
9434                }
9435                if matches!(self.peek(), Token::Comma) {
9436                    self.advance();
9437                    continue;
9438                }
9439                break;
9440            }
9441            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9442            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9443            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9444            // `public.` dump qualifiers), so the qualifier has to be read off
9445            // the token stream first — otherwise `SET b.v = 888` would write
9446            // to the TARGET table's `v` while naming a source table, a
9447            // silent-wrong. A qualifier naming a SOURCE table means a
9448            // multi-TARGET update — mutating two tables in one statement —
9449            // which SPG does not model, so it is refused loudly.
9450            let set_qual: Option<String> = if mysql_from.is_some()
9451                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9452            {
9453                match self.peek() {
9454                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9455                    _ => None,
9456                }
9457            } else {
9458                None
9459            };
9460            let col = self.expect_ident_like()?;
9461            if let Some(q) = set_qual {
9462                let names_target = q.eq_ignore_ascii_case(&table)
9463                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9464                if !names_target {
9465                    return Err(self.err(alloc::format!(
9466                        "multi-table UPDATE can only assign to its first table \
9467                         ({table}); `{q}.{col}` targets another table"
9468                    )));
9469                }
9470            }
9471            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9472            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9473            // `__column_default` marker lowering just below). PG assigns to the
9474            // i-th (1-based) element, NULL-padding when i exceeds the length.
9475            if matches!(self.peek(), Token::LBracket) {
9476                self.advance();
9477                let index = self.parse_expr(0)?;
9478                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9479                // (and the open `arr[lo:]`), lowered to
9480                // `__array_assign_slice`. Only the single-subscript form
9481                // parsed before, so a slice assignment was a syntax error.
9482                let mut slice_hi: Option<Option<Expr>> = None;
9483                if matches!(self.peek(), Token::Colon) {
9484                    self.advance();
9485                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9486                        None
9487                    } else {
9488                        Some(self.parse_expr(0)?)
9489                    });
9490                }
9491                if !matches!(self.peek(), Token::RBracket) {
9492                    return Err(self.err(format!(
9493                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9494                        self.peek()
9495                    )));
9496                }
9497                self.advance();
9498                if !matches!(self.peek(), Token::Eq) {
9499                    return Err(self.err(format!(
9500                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9501                        self.peek()
9502                    )));
9503                }
9504                self.advance();
9505                let value = self.parse_expr(0)?;
9506                // PG merges several subscript writes to the same column into one
9507                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9508                // assignment to `col` rather than each overwriting the original.
9509                let existing = assignments.iter().position(|(c, _)| c == &col);
9510                let base = match existing {
9511                    Some(i) => assignments[i].1.clone(),
9512                    None => Expr::Column(ColumnName {
9513                        qualifier: None,
9514                        name: col.clone(),
9515                    }),
9516                };
9517                let assigned = match slice_hi {
9518                    None => Expr::FunctionCall {
9519                        name: "__array_assign".to_string(),
9520                        args: alloc::vec![base, index, value],
9521                    },
9522                    Some(hi) => Expr::FunctionCall {
9523                        name: "__array_assign_slice".to_string(),
9524                        args: alloc::vec![
9525                            base,
9526                            index,
9527                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9528                            value,
9529                        ],
9530                    },
9531                };
9532                match existing {
9533                    Some(i) => assignments[i].1 = assigned,
9534                    None => assignments.push((col, assigned)),
9535                }
9536                if matches!(self.peek(), Token::Comma) {
9537                    self.advance();
9538                    continue;
9539                }
9540                break;
9541            }
9542            if !matches!(self.peek(), Token::Eq) {
9543                return Err(self.err(format!(
9544                    "expected `=` after column name in UPDATE SET, got {:?}",
9545                    self.peek()
9546                )));
9547            }
9548            self.advance();
9549            // `SET col = DEFAULT` — the column's declared default;
9550            // rides out as a marker call the update executor
9551            // resolves against the schema.
9552            let value = if matches!(self.peek(), Token::Default) {
9553                self.advance();
9554                Expr::FunctionCall {
9555                    name: "__column_default".to_string(),
9556                    args: Vec::new(),
9557                }
9558            } else {
9559                self.parse_expr(0)?
9560            };
9561            assignments.push((col, value));
9562            if matches!(self.peek(), Token::Comma) {
9563                self.advance();
9564                continue;
9565            }
9566            break;
9567        }
9568        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9569        // update. Lowers onto the correlated-subquery machinery:
9570        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9571        // and each assignment that references a FROM-list table
9572        // wraps into a correlated scalar subquery
9573        // (SELECT expr FROM src WHERE cond). Equivalent for the
9574        // unique-join shape (the overwhelmingly common one); a
9575        // multi-match, which PG resolves by arbitrary pick,
9576        // surfaces as a scalar-subquery cardinality error instead
9577        // of a silent arbitrary result.
9578        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9579        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9580        // the SAME lowering below. Both spellings together is not legal in
9581        // either dialect.
9582        let from_clause = if let Some(fc) = mysql_from {
9583            if matches!(self.peek(), Token::From) {
9584                return Err(self.err(alloc::string::String::from(
9585                    "multi-table UPDATE already names its sources; drop the FROM clause",
9586                )));
9587            }
9588            Some(fc)
9589        } else if matches!(self.peek(), Token::From) {
9590            self.advance();
9591            Some(self.parse_from_clause()?)
9592        } else {
9593            None
9594        };
9595        let where_ = if matches!(self.peek(), Token::Where) {
9596            self.advance();
9597            Some(self.parse_expr(0)?)
9598        } else {
9599            None
9600        };
9601        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9602        // and the TARGET-row filter are NOT the same predicate once a LEFT
9603        // join is involved:
9604        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9605        //     one conjunction, and the whole thing filters target rows via
9606        //     EXISTS.
9607        //   * LEFT join: only the ON predicate belongs inside the source
9608        //     subquery. The WHERE still filters TARGET rows (with source
9609        //     columns read through the correlated subquery, which yields NULL
9610        //     for an unmatched row — exactly LEFT-join semantics).
9611        // Round 420 folded ON into WHERE unconditionally and then dropped the
9612        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9613        // WHERE a.id > 1` updated EVERY row.
9614        let sub_where = match (mysql_on.clone(), where_.clone()) {
9615            _ if mysql_outer => mysql_on.clone(),
9616            (Some(on), Some(w)) => Some(Expr::Binary {
9617                lhs: Box::new(on),
9618                op: crate::ast::BinOp::And,
9619                rhs: Box::new(w),
9620            }),
9621            (Some(on), None) => Some(on),
9622            (None, w) => w,
9623        };
9624        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9625        // has no such clause on UPDATE, so this is accepted only under the
9626        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9627        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9628        let mut returning = self.parse_optional_returning()?;
9629        // v7.39 (round 533) — kept for the engine, which can resolve the
9630        // UNQUALIFIED leaves this lowering has to leave alone.
9631        let from_sources = from_clause.as_ref().map(|fc| {
9632            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9633                from: fc.clone(),
9634                sub_where: sub_where.clone(),
9635            })
9636        });
9637        let (assignments, where_) = if let Some(fc) = from_clause {
9638            let names: Vec<String> = core::iter::once(&fc.primary)
9639                .chain(fc.joins.iter().map(|j| &j.table))
9640                .flat_map(|t| {
9641                    t.alias
9642                        .clone()
9643                        .into_iter()
9644                        .chain(core::iter::once(t.name.clone()))
9645                })
9646                .collect();
9647            let refs_list = |e: &Expr| -> bool {
9648                fn walk(e: &Expr, names: &[String]) -> bool {
9649                    match e {
9650                        Expr::Column(c) => c
9651                            .qualifier
9652                            .as_deref()
9653                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9654                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9655                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9656                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9657                        Expr::Case {
9658                            operand,
9659                            branches,
9660                            else_branch,
9661                        } => {
9662                            operand.as_deref().is_some_and(|o| walk(o, names))
9663                                || branches
9664                                    .iter()
9665                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9666                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9667                        }
9668                        _ => false,
9669                    }
9670                }
9671                walk(e, &names)
9672            };
9673            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9674                locking: None,
9675                ctes: Vec::new(),
9676                distinct: false,
9677                distinct_on: Vec::new(),
9678                items,
9679                from: Some(fc.clone()),
9680                where_: sub_where.clone(),
9681                group_by: None,
9682                group_by_all: false,
9683                having: None,
9684                unions: Vec::new(),
9685                order_by: Vec::new(),
9686                limit: None,
9687                offset: None,
9688                limit_with_ties: false,
9689                window_check_exprs: Vec::new(),
9690            };
9691            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9692            // assignment RHS with a correlated scalar subquery, instead of
9693            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9694            // column reference (`SET v = v + u.bonus`, where `v` is the target
9695            // table's column) inside a subquery whose FROM only has the source
9696            // table, so the unqualified `v` resolved against the source and
9697            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9698            // context — where they belong — fixes it; only the source columns
9699            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9700            // compound variants the leaf-walk doesn't decompose.
9701            let make_subq = |inner: Expr| {
9702                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9703                    expr: inner,
9704                    alias: None,
9705                }])))
9706            };
9707            let assignments = assignments
9708                .into_iter()
9709                .map(|(col, mut expr)| {
9710                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9711                    (col, expr)
9712                })
9713                .collect();
9714            let exists = Expr::Exists {
9715                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9716                    expr: Expr::Literal(Literal::Integer(1)),
9717                    alias: None,
9718                }])),
9719                negated: false,
9720            };
9721            // v7.39 (round 241) — RETURNING may reference the FROM-list
9722            // tables too (`RETURNING emp.id, dept.name`); the same
9723            // leaf-to-correlated-subquery lowering the assignments get.
9724            // Without it the qualifier died at eval with "unknown table
9725            // qualifier". (RETURNING was parsed before this block — the
9726            // lowering is a pure AST transformation.)
9727            if let Some(items) = returning.as_mut() {
9728                for item in items.iter_mut() {
9729                    if let SelectItem::Expr { expr, .. } = item {
9730                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9731                    }
9732                }
9733            }
9734            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9735            // EVERY matching target row: it gets no EXISTS filter, but the
9736            // caller's WHERE still applies, with source columns read through
9737            // the correlated subquery (NULL when unmatched — LEFT-join
9738            // semantics). `sub_where` above already excluded the WHERE from
9739            // the source subquery for this case.
9740            if mysql_outer {
9741                let mut outer = where_;
9742                if let Some(w) = outer.as_mut() {
9743                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9744                }
9745                (assignments, outer)
9746            } else {
9747                (assignments, Some(exists))
9748            }
9749        } else {
9750            (assignments, where_)
9751        };
9752        Ok(Statement::Update(crate::ast::UpdateStatement {
9753            ctes: Vec::new(),
9754            table,
9755            only,
9756            alias,
9757            assignments,
9758            from_sources,
9759            where_,
9760            order_limit: update_order_limit,
9761            returning,
9762        }))
9763    }
9764
9765    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9766    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9767    /// clause and its meaning are identical, so both call this rather than
9768    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9769    /// legal. PG has no such clause on either statement, so it is read only
9770    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9771    /// errors.
9772    ///
9773    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9774    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9775    /// stack in round 430.
9776    #[inline(never)]
9777    fn parse_mysql_dml_order_limit(
9778        &mut self,
9779        what: &str,
9780    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9781        if !self.mysql_dialect {
9782            return Ok(None);
9783        }
9784        let order_by = self.parse_order_by_keys()?;
9785        let limit = if matches!(self.peek(), Token::Limit) {
9786            self.advance();
9787            let tok = self.advance();
9788            let Token::Integer(n) = tok else {
9789                return Err(self.err(alloc::format!(
9790                    "expected integer after {what} LIMIT, got {tok:?}"
9791                )));
9792            };
9793            // MySQL rejects the `LIMIT offset, count` form here — only a
9794            // single row count is legal on a DML statement.
9795            if matches!(self.peek(), Token::Comma) {
9796                return Err(self.err(alloc::format!(
9797                    "{what} LIMIT takes a row count, not an offset"
9798                )));
9799            }
9800            let n = u32::try_from(n)
9801                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9802            Some(n)
9803        } else {
9804            None
9805        };
9806        if order_by.is_empty() && limit.is_none() {
9807            return Ok(None);
9808        }
9809        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9810            order_by,
9811            limit,
9812        })))
9813    }
9814
9815    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9816    /// the leading `DELETE` ident.
9817    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9818        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9819        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9820        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9821        // parse here; it reaches the existing USING path with the target
9822        // repeated in the list, which the source-list peel below handles.)
9823        // More than one name is a multi-TARGET delete, which SPG does not
9824        // model; it is refused rather than half-applied.
9825        let mysql_pre_target: Option<String> =
9826            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9827                let first = self.expect_ident_like()?;
9828                if matches!(self.peek(), Token::Comma) {
9829                    return Err(self.err(alloc::format!(
9830                        "multi-table DELETE can only delete from one table; \
9831                     `DELETE {first}, …` names several"
9832                    )));
9833                }
9834                Some(first)
9835            } else {
9836                None
9837            };
9838        if !matches!(self.peek(), Token::From) {
9839            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9840        }
9841        self.advance();
9842        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9843        // lookahead as the UPDATE spelling.
9844        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9845            if s.eq_ignore_ascii_case("only"))
9846            && matches!(
9847                self.tokens.get(self.pos + 1),
9848                Some(Token::Ident(_) | Token::QuotedIdent(_))
9849            );
9850        if only {
9851            self.advance();
9852        }
9853        let table = self.expect_ident_like()?;
9854        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9855        // spelling must not swallow the clause keywords that can follow
9856        // the target.
9857        let alias = if matches!(self.peek(), Token::As) {
9858            self.advance();
9859            Some(self.expect_ident_like()?)
9860        } else {
9861            match self.peek() {
9862                Token::Ident(s) | Token::QuotedIdent(s)
9863                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9864                {
9865                    let a = s.clone();
9866                    self.advance();
9867                    Some(a)
9868                }
9869                _ => None,
9870            }
9871        };
9872        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9873        // through the SAME join grammar the FROM clause uses (see the
9874        // `advance()`-destroys-tokens note on `parse_from_joins`).
9875        let mut mysql_on: Option<Expr> = None;
9876        let mut mysql_outer = false;
9877        let mysql_using = if mysql_pre_target.is_some()
9878            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9879        {
9880            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9881            let mut joins = self.parse_from_joins(&target_qual)?;
9882            if joins.is_empty() {
9883                return Err(self.err(alloc::string::String::from(
9884                    "multi-table DELETE needs at least one source table",
9885                )));
9886            }
9887            let head = joins.remove(0);
9888            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9889            mysql_on = head.on;
9890            Some(FromClause {
9891                primary: head.table,
9892                joins,
9893            })
9894        } else {
9895            None
9896        };
9897        // The pre-FROM target must be the table the FROM names (or its
9898        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9899        // is not the scan target.
9900        if let Some(t) = &mysql_pre_target {
9901            let names_target = t.eq_ignore_ascii_case(&table)
9902                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9903            if !names_target {
9904                return Err(self.err(alloc::format!(
9905                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9906                )));
9907            }
9908        }
9909        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9910        // delete. Same lowering as UPDATE … FROM: the WHERE
9911        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9912        // target row by the correlated machinery.
9913        let using_clause = if let Some(fc) = mysql_using {
9914            Some(fc)
9915        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9916            self.advance();
9917            let mut fc = self.parse_from_clause()?;
9918            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9919            // repeats the TARGET as the first USING entry (PG's spelling
9920            // lists only the extra sources). Peel it so the source subquery
9921            // does not re-scan — and shadow — the target table.
9922            let primary_is_target =
9923                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9924            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9925                let head = fc.joins.remove(0);
9926                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9927                mysql_on = head.on;
9928                fc = FromClause {
9929                    primary: head.table,
9930                    joins: fc.joins,
9931                };
9932            }
9933            Some(fc)
9934        } else {
9935            None
9936        };
9937        let where_ = if matches!(self.peek(), Token::Where) {
9938            self.advance();
9939            Some(self.parse_expr(0)?)
9940        } else {
9941            None
9942        };
9943        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9944        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9945        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9946        let mut returning = self.parse_optional_returning()?;
9947        let where_ = if let Some(fc) = using_clause {
9948            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9949            // a USING-table reference in RETURNING becomes a correlated
9950            // scalar subquery over the USING list.
9951            let names: Vec<String> = core::iter::once(&fc.primary)
9952                .chain(fc.joins.iter().map(|j| &j.table))
9953                .flat_map(|t| {
9954                    t.alias
9955                        .clone()
9956                        .into_iter()
9957                        .chain(core::iter::once(t.name.clone()))
9958                })
9959                .collect();
9960            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9961            // join filters the SOURCE subquery on the ON predicate alone and
9962            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9963            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9964            // rows); every other form folds ON and WHERE into one EXISTS.
9965            let sub_where = match (mysql_on.clone(), where_.clone()) {
9966                _ if mysql_outer => mysql_on.clone(),
9967                (Some(on), Some(w)) => Some(Expr::Binary {
9968                    lhs: Box::new(on),
9969                    op: crate::ast::BinOp::And,
9970                    rhs: Box::new(w),
9971                }),
9972                (Some(on), None) => Some(on),
9973                (None, w) => w,
9974            };
9975            let exists_where = sub_where.clone();
9976            let sub_fc = fc.clone();
9977            let make_subq = move |leaf: Expr| -> Expr {
9978                Expr::ScalarSubquery(Box::new(SelectStatement {
9979                    locking: None,
9980                    ctes: Vec::new(),
9981                    distinct: false,
9982                    distinct_on: Vec::new(),
9983                    items: alloc::vec![SelectItem::Expr {
9984                        expr: leaf,
9985                        alias: None,
9986                    }],
9987                    from: Some(sub_fc.clone()),
9988                    where_: sub_where.clone(),
9989                    group_by: None,
9990                    group_by_all: false,
9991                    having: None,
9992                    unions: Vec::new(),
9993                    order_by: Vec::new(),
9994                    limit: None,
9995                    offset: None,
9996                    limit_with_ties: false,
9997                    window_check_exprs: Vec::new(),
9998                }))
9999            };
10000            let refs = |e: &Expr| expr_refs_tables(e, &names);
10001            if let Some(items) = returning.as_mut() {
10002                for item in items.iter_mut() {
10003                    if let SelectItem::Expr { expr, .. } = item {
10004                        wrap_from_leaves(expr, &names, &make_subq, &refs);
10005                    }
10006                }
10007            }
10008            // A LEFT join deletes the target rows the WHERE selects, reading
10009            // source columns through the correlated subquery (NULL when
10010            // unmatched); no EXISTS row filter.
10011            if mysql_outer {
10012                let mut outer = where_;
10013                if let Some(w) = outer.as_mut() {
10014                    wrap_from_leaves(w, &names, &make_subq, &refs);
10015                }
10016                outer
10017            } else {
10018                Some(Expr::Exists {
10019                    subquery: Box::new(SelectStatement {
10020                        locking: None,
10021                        ctes: Vec::new(),
10022                        distinct: false,
10023                        distinct_on: Vec::new(),
10024                        items: alloc::vec![SelectItem::Expr {
10025                            expr: Expr::Literal(Literal::Integer(1)),
10026                            alias: None,
10027                        }],
10028                        from: Some(fc),
10029                        where_: exists_where,
10030                        group_by: None,
10031                        group_by_all: false,
10032                        having: None,
10033                        unions: Vec::new(),
10034                        order_by: Vec::new(),
10035                        limit: None,
10036                        offset: None,
10037                        limit_with_ties: false,
10038                        window_check_exprs: Vec::new(),
10039                    }),
10040                    negated: false,
10041                })
10042            }
10043        } else {
10044            where_
10045        };
10046        Ok(Statement::Delete(crate::ast::DeleteStatement {
10047            ctes: Vec::new(),
10048            table,
10049            only,
10050            alias,
10051            where_,
10052            order_limit: delete_order_limit,
10053            returning,
10054        }))
10055    }
10056
10057    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
10058    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
10059    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
10060    /// keyword. v7.17 surface:
10061    ///   * source: table reference (subquery source is a follow-up)
10062    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
10063    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
10064    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
10065    ///     order
10066    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
10067        // INTO
10068        let is_into_kw = matches!(self.peek(), Token::Into)
10069            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
10070        if !is_into_kw {
10071            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
10072        }
10073        self.advance();
10074        let target = self.expect_ident_like()?;
10075        // Optional alias — bare ident before USING.
10076        let target_alias = match self.peek() {
10077            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10078                Some(self.expect_ident_like()?)
10079            }
10080            _ => None,
10081        };
10082        // USING
10083        let is_using_kw = matches!(
10084            self.peek(),
10085            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10086        );
10087        if !is_using_kw {
10088            return Err(self.err(format!(
10089                "expected USING after MERGE INTO target, got {:?}",
10090                self.peek()
10091            )));
10092        }
10093        self.advance();
10094        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10095        // <table> [alias]`. PG requires an alias after a subquery source.
10096        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10097            self.advance(); // (
10098            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10099            // constant-SELECT lowering the derived-table parser uses
10100            // (PG deletes through this form; it was a parse error).
10101            let inner = if matches!(self.peek(), Token::Values) {
10102                self.advance(); // VALUES
10103                Statement::Select(self.parse_values_rows_body()?)
10104            } else {
10105                self.parse_select_stmt()?
10106            };
10107            match self.advance() {
10108                Token::RParen => {}
10109                other => {
10110                    return Err(self.err(format!(
10111                        "expected ')' after MERGE USING subquery, got {other:?}"
10112                    )));
10113                }
10114            }
10115            let Statement::Select(sub) = inner else {
10116                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10117            };
10118            (String::new(), Some(Box::new(sub)))
10119        } else {
10120            (self.expect_ident_like()?, None)
10121        };
10122        let source_alias = match self.peek() {
10123            Token::Ident(s) | Token::QuotedIdent(s)
10124                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10125            {
10126                Some(self.expect_ident_like()?)
10127            }
10128            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10129                self.advance(); // AS
10130                Some(self.expect_ident_like()?)
10131            }
10132            _ => None,
10133        };
10134        // v7.39 (round 768, F31-D5) — optional positional column-alias
10135        // list after the source alias (`s(id, v)`).
10136        let mut source_column_aliases: Vec<String> = Vec::new();
10137        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10138            self.advance();
10139            loop {
10140                source_column_aliases.push(self.expect_ident_like()?);
10141                match self.peek() {
10142                    Token::Comma => {
10143                        self.advance();
10144                    }
10145                    Token::RParen => {
10146                        self.advance();
10147                        break;
10148                    }
10149                    other => {
10150                        return Err(self.err(format!(
10151                            "expected ',' or ')' in MERGE source column list, got {other:?}"
10152                        )));
10153                    }
10154                }
10155            }
10156        }
10157        if source_select.is_some() && source_alias.is_none() {
10158            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10159        }
10160        // ON
10161        if !matches!(self.peek(), Token::On) {
10162            return Err(self.err(format!(
10163                "expected ON after MERGE … USING source, got {:?}",
10164                self.peek()
10165            )));
10166        }
10167        self.advance();
10168        let on = self.parse_expr(0)?;
10169        // One or more WHEN clauses.
10170        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10171        loop {
10172            let is_when_kw = matches!(
10173                self.peek(),
10174                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10175            );
10176            if !is_when_kw {
10177                break;
10178            }
10179            self.advance(); // WHEN
10180            // [NOT] MATCHED
10181            let matched = if matches!(self.peek(), Token::Not) {
10182                self.advance();
10183                crate::ast::MergeMatched::NotMatched
10184            } else {
10185                crate::ast::MergeMatched::Matched
10186            };
10187            let is_matched_kw = matches!(
10188                self.peek(),
10189                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10190            );
10191            if !is_matched_kw {
10192                return Err(self.err(format!(
10193                    "expected MATCHED in WHEN clause, got {:?}",
10194                    self.peek()
10195                )));
10196            }
10197            self.advance();
10198            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10199            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10200            // to fire for target rows no source row matches.
10201            let mut matched = matched;
10202            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10203                self.advance();
10204                match self.peek() {
10205                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10206                        self.advance();
10207                        matched = crate::ast::MergeMatched::NotMatchedBySource;
10208                    }
10209                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10210                        self.advance();
10211                    }
10212                    other => {
10213                        return Err(self.err(format!(
10214                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10215                        )));
10216                    }
10217                }
10218            }
10219            // Optional AND <expr>
10220            let condition = if matches!(self.peek(), Token::And) {
10221                self.advance();
10222                Some(self.parse_expr(0)?)
10223            } else {
10224                None
10225            };
10226            // THEN
10227            let is_then_kw = matches!(
10228                self.peek(),
10229                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10230            );
10231            if !is_then_kw {
10232                return Err(self.err(format!(
10233                    "expected THEN in WHEN clause, got {:?}",
10234                    self.peek()
10235                )));
10236            }
10237            self.advance();
10238            // Action: INSERT / UPDATE / DELETE / DO NOTHING
10239            let action = match self.peek().clone() {
10240                Token::Insert => {
10241                    self.advance();
10242                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10243                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10244                    // VALUES (…)` omits it and fills every column in declaration
10245                    // order. PG accepts this; SPG used to require the list.
10246                    let mut columns: Vec<String> = Vec::new();
10247                    if matches!(self.peek(), Token::LParen) {
10248                        self.advance();
10249                        loop {
10250                            columns.push(self.expect_ident_like()?);
10251                            if matches!(self.peek(), Token::Comma) {
10252                                self.advance();
10253                                continue;
10254                            }
10255                            break;
10256                        }
10257                        if !matches!(self.peek(), Token::RParen) {
10258                            return Err(self.err(format!(
10259                                "expected ')' after INSERT column list, got {:?}",
10260                                self.peek()
10261                            )));
10262                        }
10263                        self.advance();
10264                    }
10265                    // VALUES (...)
10266                    if !matches!(self.peek(), Token::Values) {
10267                        return Err(self.err(format!(
10268                            "expected VALUES in MERGE INSERT, got {:?}",
10269                            self.peek()
10270                        )));
10271                    }
10272                    self.advance();
10273                    if !matches!(self.peek(), Token::LParen) {
10274                        return Err(self.err(format!(
10275                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
10276                            self.peek()
10277                        )));
10278                    }
10279                    self.advance();
10280                    let mut values: Vec<crate::ast::Expr> = Vec::new();
10281                    loop {
10282                        values.push(self.parse_expr(0)?);
10283                        if matches!(self.peek(), Token::Comma) {
10284                            self.advance();
10285                            continue;
10286                        }
10287                        break;
10288                    }
10289                    if !matches!(self.peek(), Token::RParen) {
10290                        return Err(self.err(format!(
10291                            "expected ')' after MERGE INSERT values, got {:?}",
10292                            self.peek()
10293                        )));
10294                    }
10295                    self.advance();
10296                    // Empty column list = positional into every column, so the
10297                    // count is checked against the table arity at execution.
10298                    if !columns.is_empty() && columns.len() != values.len() {
10299                        return Err(self.err(format!(
10300                            "MERGE INSERT column count ({}) ≠ value count ({})",
10301                            columns.len(),
10302                            values.len()
10303                        )));
10304                    }
10305                    crate::ast::MergeAction::Insert { columns, values }
10306                }
10307                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10308                    self.advance();
10309                    // SET
10310                    let is_set_kw = matches!(
10311                        self.peek(),
10312                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10313                    );
10314                    if !is_set_kw {
10315                        return Err(self.err(format!(
10316                            "expected SET after UPDATE in MERGE, got {:?}",
10317                            self.peek()
10318                        )));
10319                    }
10320                    self.advance();
10321                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10322                    loop {
10323                        let col = self.expect_ident_like()?;
10324                        if !matches!(self.peek(), Token::Eq) {
10325                            return Err(self.err(format!(
10326                                "expected '=' in MERGE UPDATE assignment, got {:?}",
10327                                self.peek()
10328                            )));
10329                        }
10330                        self.advance();
10331                        let expr = self.parse_expr(0)?;
10332                        assignments.push((col, expr));
10333                        if matches!(self.peek(), Token::Comma) {
10334                            self.advance();
10335                            continue;
10336                        }
10337                        break;
10338                    }
10339                    crate::ast::MergeAction::Update { assignments }
10340                }
10341                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10342                    self.advance();
10343                    crate::ast::MergeAction::Delete
10344                }
10345                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10346                    self.advance();
10347                    let is_nothing_kw = matches!(
10348                        self.peek(),
10349                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10350                    );
10351                    if !is_nothing_kw {
10352                        return Err(self.err(format!(
10353                            "expected NOTHING after DO in MERGE clause, got {:?}",
10354                            self.peek()
10355                        )));
10356                    }
10357                    self.advance();
10358                    crate::ast::MergeAction::DoNothing
10359                }
10360                other => {
10361                    return Err(self.err(format!(
10362                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10363                    )));
10364                }
10365            };
10366            // PG's grammar simply has no INSERT production under BY SOURCE
10367            // (a target row already exists there) — same syntax error.
10368            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10369                && matches!(action, crate::ast::MergeAction::Insert { .. })
10370            {
10371                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10372            }
10373            clauses.push(crate::ast::MergeWhenClause {
10374                matched,
10375                condition,
10376                action,
10377            });
10378        }
10379        if clauses.is_empty() {
10380            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10381        }
10382        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10383        // unconditional (no `AND`) WHEN of the same match kind: it could
10384        // never fire. Check per match kind in clause order.
10385        let mut seen_unconditional_matched = false;
10386        let mut seen_unconditional_not_matched = false;
10387        let mut seen_unconditional_by_source = false;
10388        for c in &clauses {
10389            let seen = match c.matched {
10390                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10391                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10392                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10393            };
10394            if *seen {
10395                return Err(self.err(String::from(
10396                    "unreachable WHEN clause specified after unconditional WHEN clause",
10397                )));
10398            }
10399            if c.condition.is_none() {
10400                *seen = true;
10401            }
10402        }
10403        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10404        let returning = self.parse_optional_returning()?;
10405        Ok(Statement::Merge(crate::ast::MergeStatement {
10406            // Attached by `parse_with_cte_then_select` when the MERGE
10407            // heads a WITH clause (round 149).
10408            ctes: Vec::new(),
10409            target,
10410            target_alias,
10411            source,
10412            source_alias,
10413            source_select,
10414            source_column_aliases,
10415            on,
10416            clauses,
10417            returning,
10418        }))
10419    }
10420
10421    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10422    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10423    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10424    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10425    fn parse_optional_returning(
10426        &mut self,
10427    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10428        let is_returning_kw = matches!(
10429            self.peek(),
10430            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10431        );
10432        if !is_returning_kw {
10433            return Ok(None);
10434        }
10435        self.advance();
10436        let mut items = Vec::new();
10437        loop {
10438            items.push(self.parse_select_item()?);
10439            if matches!(self.peek(), Token::Comma) {
10440                self.advance();
10441                continue;
10442            }
10443            break;
10444        }
10445        Ok(Some(items))
10446    }
10447
10448    /// v6.0.4 — parse the tail of an ALTER statement after the
10449    /// leading `ALTER` keyword has been consumed. Only one form is
10450    /// supported in v6.0.4:
10451    ///
10452    /// ```text
10453    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10454    /// ```
10455    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10456        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10457        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10458        // exclusion) is accepted by stripping the `ONLY` keyword
10459        // before the table parse.
10460        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10461        // and the long PG-dump tail are accepted as no-ops.
10462        match self.advance() {
10463            Token::Index => {}
10464            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10465            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10466            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10467            Token::Table => {
10468                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10469                    self.advance();
10470                }
10471                return self.parse_alter_table_after_keyword();
10472            }
10473            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10474                return self.parse_alter_policy_after_keyword();
10475            }
10476            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10477                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10478                    self.advance();
10479                }
10480                return self.parse_alter_table_after_keyword();
10481            }
10482            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10483            // of the silent-noop tail.
10484            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10485                return self.parse_alter_sequence_after_keyword();
10486            }
10487            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10488            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10489            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10490            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10491                // NB: the match arm consumed `TYPE` via self.advance(); the
10492                // cursor is now at the type name — do NOT advance again.
10493                let type_name = self.expect_ident_like()?;
10494                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10495                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10496                if is_add_value {
10497                    self.advance(); // ADD
10498                    self.advance(); // VALUE
10499                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10500                    // IF/EXISTS as identifiers.
10501                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10502                    {
10503                        let n1 = self.tokens.get(self.pos + 1);
10504                        let n2 = self.tokens.get(self.pos + 2);
10505                        if matches!(n1, Some(Token::Not))
10506                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10507                        {
10508                            self.advance();
10509                            self.advance();
10510                            self.advance();
10511                            true
10512                        } else {
10513                            false
10514                        }
10515                    } else {
10516                        false
10517                    };
10518                    let label = self.expect_string_literal()?;
10519                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10520                    {
10521                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10522                        self.advance();
10523                        let anchor = self.expect_string_literal()?;
10524                        Some((is_before, anchor))
10525                    } else {
10526                        None
10527                    };
10528                    return Ok(Statement::AlterTypeAddValue {
10529                        type_name,
10530                        label,
10531                        if_not_exists,
10532                        position,
10533                    });
10534                }
10535                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10536                // Used to fall into the no-op tail below: accepted, silently
10537                // ignored. `RENAME TO <newtype>` keeps falling through.
10538                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10539                    && matches!(
10540                        self.tokens.get(self.pos + 1),
10541                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10542                    )
10543                {
10544                    self.advance(); // RENAME
10545                    self.advance(); // VALUE
10546                    let old = self.expect_string_literal()?;
10547                    if matches!(self.peek(), Token::To) {
10548                        self.advance();
10549                    } else {
10550                        self.expect_keyword_ident("to")?;
10551                    }
10552                    let new = self.expect_string_literal()?;
10553                    return Ok(Statement::AlterTypeRenameValue {
10554                        type_name,
10555                        old,
10556                        new,
10557                    });
10558                }
10559                // Other ALTER TYPE forms — the ACTION stays a no-op
10560                // (pg_dump tail), but v7.39 (round 708) the NAME is
10561                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10562                // success for a type that does not exist.
10563                self.consume_until_statement_boundary();
10564                return Ok(Statement::ValidateOnly {
10565                    kind: crate::ast::ValidateOnlyKind::TypeName,
10566                    names: alloc::vec![type_name],
10567                });
10568            }
10569            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10570            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10571            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10572            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10573            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10574            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10575            // pg_dump no-op list below: every form used to report success
10576            // and change nothing, which is worse than refusing outright
10577            // (a migration dropping a constraint kept rejecting data).
10578            // NOTE: the enclosing `match self.advance()` already consumed
10579            // the DOMAIN keyword, so the name is next.
10580            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10581                return self.parse_alter_domain_after_keyword();
10582            }
10583            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10584            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10585            // used to fall into the pg_dump no-op tail below, so a DBA
10586            // setting a per-role default was told it worked and nothing
10587            // happened. Intercepted here, BEFORE that tail.
10588            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10589            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10590            // interception below exists: swallowed with the no-op tail, an
10591            // unknown parameter name was ACCEPTED where PG18 answers
10592            // `unrecognized configuration parameter`. SPG applies nothing
10593            // either way — there is no postgresql.auto.conf — but it now
10594            // says so about a name it does not know.
10595            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10596                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10597                // already consumed here. An extra advance eats the SET and
10598                // the parameter name is never seen — which is exactly the
10599                // bug a panic in this branch disproved: the branch WAS on
10600                // the path, the reading of it was wrong.
10601                let mut parameter = None;
10602                // SET <name> … | RESET <name> | RESET ALL
10603                if matches!(self.peek(), Token::Ident(k)
10604                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10605                {
10606                    self.advance();
10607                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10608                        && !n.eq_ignore_ascii_case("all")
10609                    {
10610                        self.advance();
10611                        // A dotted GUC (`plpgsql.check_asserts`) is two
10612                        // tokens; keep the whole name.
10613                        let mut full = n;
10614                        while matches!(self.peek(), Token::Dot) {
10615                            self.advance();
10616                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10617                                full.push('.');
10618                                full.push_str(&t);
10619                            }
10620                        }
10621                        parameter = Some(full);
10622                    }
10623                }
10624                self.consume_until_statement_boundary();
10625                return Ok(Statement::AlterSystem { parameter });
10626            }
10627            Token::Ident(s) | Token::QuotedIdent(s)
10628                if matches!(
10629                    s.to_ascii_lowercase().as_str(),
10630                    "role" | "user" | "database"
10631                ) && self.peeks_db_role_setting() =>
10632            {
10633                let is_database = s.eq_ignore_ascii_case("database");
10634                return self.parse_db_role_setting(is_database);
10635            }
10636            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10637            // (the non-SET forms; SET/RESET took the branch above). The
10638            // attributes still no-op — recorded, and the ignored PASSWORD
10639            // is ledgered as its own follow-up — but the ROLE is validated:
10640            // any name was accepted for a role that does not exist.
10641            Token::Ident(s) | Token::QuotedIdent(s)
10642                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10643            {
10644                // NB: the enclosing `match self.advance()` already consumed
10645                // ROLE/USER — the round-695 trap, hit again in this round's
10646                // first draft (the name was eaten and WITH parsed as the
10647                // role). The cursor is at the name.
10648                let name = self.expect_ident_or_string()?;
10649                // v7.39 (round 750) — scan the attribute tail for
10650                // PASSWORD. Everything else stays a recorded no-op, but
10651                // a dropped credential rotation is a SECURITY bug:
10652                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10653                // changed nothing, so the old password kept working.
10654                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10655                // NULL` clears the credential.
10656                let mut password: Option<Option<String>> = None;
10657                loop {
10658                    match self.peek() {
10659                        Token::Semicolon | Token::Eof => break,
10660                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10661                            self.advance();
10662                            match self.advance() {
10663                                Token::String(p) => password = Some(Some(p)),
10664                                Token::Null => password = Some(None),
10665                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10666                                    password = Some(None);
10667                                }
10668                                other => {
10669                                    return Err(self.err(alloc::format!(
10670                                        "expected password string or NULL after PASSWORD, got {other:?}"
10671                                    )));
10672                                }
10673                            }
10674                        }
10675                        _ => {
10676                            self.advance();
10677                        }
10678                    }
10679                }
10680                if name.eq_ignore_ascii_case("all") {
10681                    // `ALTER ROLE ALL …` names every role; nothing to check.
10682                    return Ok(Statement::Empty);
10683                }
10684                if let Some(pw) = password {
10685                    return Ok(Statement::AlterRolePassword { name, password: pw });
10686                }
10687                return Ok(Statement::ValidateOnly {
10688                    kind: crate::ast::ValidateOnlyKind::RoleName,
10689                    names: alloc::vec![name],
10690                });
10691            }
10692            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10693            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10694            // list far enough to validate the NAME; the actions still no-op.
10695            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10696            // models none of them and their dumps are rare.)
10697            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10698                let name = self.expect_ident_or_string()?;
10699                self.consume_until_statement_boundary();
10700                return Ok(Statement::ValidateOnly {
10701                    kind: crate::ast::ValidateOnlyKind::CollationName,
10702                    names: alloc::vec![name],
10703                });
10704            }
10705            Token::Ident(s) | Token::QuotedIdent(s)
10706                if s.eq_ignore_ascii_case("text")
10707                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10708                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10709            {
10710                self.advance(); // SEARCH
10711                self.advance(); // CONFIGURATION
10712                let name = self.expect_ident_like()?;
10713                self.consume_until_statement_boundary();
10714                return Ok(Statement::ValidateOnly {
10715                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10716                    names: alloc::vec![name],
10717                });
10718            }
10719            Token::Ident(s) | Token::QuotedIdent(s)
10720                if s.eq_ignore_ascii_case("event")
10721                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10722            {
10723                self.advance(); // TRIGGER
10724                let name = self.expect_ident_like()?;
10725                self.consume_until_statement_boundary();
10726                return Ok(Statement::ValidateOnly {
10727                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10728                    names: alloc::vec![name],
10729                });
10730            }
10731            Token::Ident(s) | Token::QuotedIdent(s)
10732                if s.eq_ignore_ascii_case("large")
10733                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10734            {
10735                self.advance(); // OBJECT
10736                let oid = match self.advance() {
10737                    Token::Integer(n) => alloc::format!("{n}"),
10738                    other => {
10739                        return Err(
10740                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10741                        );
10742                    }
10743                };
10744                self.consume_until_statement_boundary();
10745                return Ok(Statement::ValidateOnly {
10746                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10747                    names: alloc::vec![oid],
10748                });
10749            }
10750            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10751            // argument-list parse as DROP AGGREGATE (round 707); the
10752            // action no-ops, the existence check is real.
10753            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10754                // Same round-695 trap as above: AGGREGATE is already
10755                // consumed; the cursor is at the name.
10756                let name = self.expect_ident_like()?;
10757                let mut names = alloc::vec![name];
10758                if matches!(self.peek(), Token::LParen) {
10759                    self.advance();
10760                    loop {
10761                        match self.peek().clone() {
10762                            Token::RParen => {
10763                                self.advance();
10764                                break;
10765                            }
10766                            Token::Star => {
10767                                self.advance();
10768                                names.push(String::from("*"));
10769                            }
10770                            Token::Comma => {
10771                                self.advance();
10772                            }
10773                            _ => {
10774                                let mut t = self.expect_ident_like()?;
10775                                while let Token::Ident(nx) = self.peek() {
10776                                    let nx = nx.clone();
10777                                    self.advance();
10778                                    t.push(' ');
10779                                    t.push_str(&nx);
10780                                }
10781                                names.push(t);
10782                            }
10783                        }
10784                    }
10785                }
10786                self.consume_until_statement_boundary();
10787                return Ok(Statement::ValidateOnly {
10788                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10789                    names,
10790                });
10791            }
10792            Token::Ident(s) | Token::QuotedIdent(s)
10793                if matches!(
10794                    s.to_ascii_lowercase().as_str(),
10795                    "view"
10796                        | "function"
10797                        | "database"
10798                        | "schema"
10799                        | "owner"
10800                        | "default"
10801                        | "extension"
10802                        | "materialized"
10803                        | "publication"
10804                        | "subscription"
10805                        // v7.37.17 (17.6 siblings) — additional ALTER
10806                        // targets pg_dump / pg_dumpall / operator DB
10807                        // migration scripts commonly emit. SPG has
10808                        // no matching machinery for any of these; the
10809                        // parser accepts + Empty-returns so pg_dump
10810                        // tail statements don't stall.
10811                        | "tablespace"
10812                        | "language"
10813                        | "operator"
10814                        | "conversion"
10815                        | "statistics"
10816                        | "server"
10817                        | "foreign"
10818                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10819                        // / TEMPLATE (CONFIGURATION intercepted above).
10820                        | "text"
10821                ) =>
10822            {
10823                self.consume_until_statement_boundary();
10824                return Ok(Statement::Empty);
10825            }
10826            other => {
10827                return Err(self.err(format!(
10828                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10829                     after ALTER, got {other:?}"
10830                )));
10831            }
10832        }
10833        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10834        // (mailrs migrate-042 ships these). The presence of an
10835        // IF EXISTS makes the subsequent name lookup tolerate
10836        // a missing index — engine returns CommandOk no-op.
10837        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10838            let next = self.tokens.get(self.pos + 1);
10839            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10840                self.advance();
10841                self.advance();
10842                true
10843            } else {
10844                false
10845            }
10846        } else {
10847            false
10848        };
10849        let name = self.expect_ident_like()?;
10850        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10851        // Detect BEFORE the REBUILD path so the existing REBUILD
10852        // arm stays untouched.
10853        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10854            self.advance();
10855            if matches!(self.peek(), Token::To) {
10856                self.advance();
10857            } else {
10858                self.expect_keyword_ident("to")?;
10859            }
10860            let new = self.expect_ident_like()?;
10861            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10862                name,
10863                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10864            }));
10865        }
10866        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10867        // A syntax error before; the index is validated, the params no-op.
10868        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10869            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10870                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10871        {
10872            self.consume_until_statement_boundary();
10873            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10874                name,
10875                target: crate::ast::AlterIndexTarget::StorageParams,
10876            }));
10877        }
10878        // REBUILD
10879        self.expect_keyword_ident("rebuild")?;
10880        // Optional: WITH (encoding = <enc>)
10881        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10882            self.advance();
10883            if !matches!(self.peek(), Token::LParen) {
10884                return Err(self.err(format!(
10885                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10886                    self.peek()
10887                )));
10888            }
10889            self.advance();
10890            self.expect_keyword_ident("encoding")?;
10891            if !matches!(self.peek(), Token::Eq) {
10892                return Err(self.err(format!(
10893                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10894                    self.peek()
10895                )));
10896            }
10897            self.advance();
10898            let enc_ident = match self.advance() {
10899                Token::Ident(s) | Token::QuotedIdent(s) => s,
10900                other => {
10901                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10902                }
10903            };
10904            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10905                "f32" => VecEncoding::F32,
10906                "sq8" => VecEncoding::Sq8,
10907                "half" => VecEncoding::F16,
10908                other => {
10909                    return Err(self.err(format!(
10910                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10911                    )));
10912                }
10913            };
10914            if !matches!(self.peek(), Token::RParen) {
10915                return Err(self.err(format!(
10916                    "expected ')' after encoding value, got {:?}",
10917                    self.peek()
10918                )));
10919            }
10920            self.advance();
10921            Some(enc)
10922        } else {
10923            None
10924        };
10925        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10926            name,
10927            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10928        }))
10929    }
10930
10931    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10932    /// only `SET` form currently supported; future v6.7.x can add
10933    /// more SET subjects without changing the dispatch shape.
10934    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10935    /// subactions. Single-subaction shape stays a 1-element vec.
10936    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10937        let table_name = self.expect_ident_like()?;
10938        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10939        loop {
10940            let subaction = self.parse_alter_table_subaction()?;
10941            // ADD COLUMN with inline REFERENCES emits both an
10942            // AddColumn and an AddForeignKey subaction; the
10943            // helper returns 1 or 2 items.
10944            targets.extend(subaction);
10945            if matches!(self.peek(), Token::Comma) {
10946                self.advance();
10947                continue;
10948            }
10949            break;
10950        }
10951        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10952            name: table_name,
10953            targets,
10954        }))
10955    }
10956
10957    /// Parse one ALTER TABLE subaction. Returns a Vec because
10958    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10959    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10960    /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>` trailer on ADD /
10961    /// MODIFY / CHANGE COLUMN. Absent is the PostgreSQL form, which
10962    /// appends.
10963    fn parse_column_position(&mut self) -> Option<crate::ast::ColumnPosition> {
10964        match self.peek() {
10965            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
10966                self.advance();
10967                Some(crate::ast::ColumnPosition::First)
10968            }
10969            Token::Ident(s) if s.eq_ignore_ascii_case("after") => {
10970                self.advance();
10971                let name = self.expect_ident_like().ok()?;
10972                Some(crate::ast::ColumnPosition::After(name))
10973            }
10974            _ => None,
10975        }
10976    }
10977
10978    fn parse_alter_table_subaction(
10979        &mut self,
10980    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
10981        match self.peek() {
10982            // v7.39.9 — MySQL's own ALTER TABLE vocabulary. Each one is
10983            // a statement a real migration emits and SPG answered 1064
10984            // for; measured against MySQL 9.7.2, one at a time, beside
10985            // the published image.
10986            Token::Ident(s)
10987                if s.eq_ignore_ascii_case("modify") || s.eq_ignore_ascii_case("change") =>
10988            {
10989                let changing = s.eq_ignore_ascii_case("change");
10990                self.advance();
10991                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("column")) {
10992                    self.advance();
10993                }
10994                // `parse_column_def_with_fk` reads the NAME itself, so
10995                // `MODIFY` hands it the column and `CHANGE` eats the old
10996                // name first and lets it read the new one.
10997                let old_name = if changing {
10998                    Some(self.expect_ident_like()?)
10999                } else {
11000                    None
11001                };
11002                let (definition, _fk) = self.parse_column_def_with_fk()?;
11003                let column = old_name.clone().unwrap_or_else(|| definition.name.clone());
11004                let rename_to = if changing {
11005                    Some(definition.name.clone())
11006                } else {
11007                    None
11008                };
11009                let position = self.parse_column_position();
11010                Ok(alloc::vec![crate::ast::AlterTableTarget::ModifyColumn {
11011                    column,
11012                    rename_to,
11013                    definition,
11014                    position,
11015                }])
11016            }
11017            Token::Ident(s) if s.eq_ignore_ascii_case("auto_increment") => {
11018                self.advance();
11019                if matches!(self.peek(), Token::Eq) {
11020                    self.advance();
11021                }
11022                let n = self.expect_u64_literal()?;
11023                Ok(alloc::vec![
11024                    crate::ast::AlterTableTarget::SetTableAutoIncrement(
11025                        i64::try_from(n).unwrap_or(i64::MAX)
11026                    )
11027                ])
11028            }
11029            Token::Ident(s) if s.eq_ignore_ascii_case("engine") => {
11030                self.advance();
11031                if matches!(self.peek(), Token::Eq) {
11032                    self.advance();
11033                }
11034                // v7.39.10 — as WRITTEN, the way `CREATE TABLE`'s ENGINE
11035                // clause has kept it since v7.39.3. The lexer folds a
11036                // bare identifier, and MySQL names the engine back
11037                // exactly: measured, `ALTER TABLE f1 ENGINE=NoSuchEng`
11038                // answers `Unknown storage engine 'NoSuchEng'` there and
11039                // answered `'nosucheng'` here — the one thing that
11040                // message is for is telling the operator which word in
11041                // their migration was wrong.
11042                let at = self.pos;
11043                let name = self.expect_ident_like()?;
11044                let written = self
11045                    .source_span(at, at)
11046                    .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
11047                    .filter(|raw| raw.eq_ignore_ascii_case(&name))
11048                    .map(alloc::string::String::from);
11049                Ok(alloc::vec![crate::ast::AlterTableTarget::SetEngine(
11050                    written.unwrap_or(name)
11051                )])
11052            }
11053            Token::Ident(s) if s.eq_ignore_ascii_case("convert") => {
11054                self.advance();
11055                // CONVERT TO CHARACTER SET <cs> [COLLATE <c>]
11056                if matches!(self.peek(), Token::To) {
11057                    self.advance();
11058                }
11059                let kw = self.expect_ident_like()?;
11060                if !kw.eq_ignore_ascii_case("character") {
11061                    return Err(self.err("expected CHARACTER after CONVERT TO".into()));
11062                }
11063                let set_kw = self.expect_ident_like()?;
11064                if !set_kw.eq_ignore_ascii_case("set") {
11065                    return Err(self.err("expected SET after CHARACTER".into()));
11066                }
11067                let charset = self.expect_ident_like()?;
11068                let collate =
11069                    if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("collate")) {
11070                        self.advance();
11071                        Some(self.expect_ident_like()?)
11072                    } else {
11073                        None
11074                    };
11075                Ok(alloc::vec![
11076                    crate::ast::AlterTableTarget::ConvertToCharacterSet { charset, collate }
11077                ])
11078            }
11079            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11080                self.advance();
11081                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
11082                // storage parameters: paren-prefixed; consume.
11083                if matches!(self.peek(), Token::LParen) {
11084                    self.consume_until_statement_boundary();
11085                    return Ok(Vec::new());
11086                }
11087                let setting = self.expect_ident_like()?;
11088                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
11089                    if !matches!(self.peek(), Token::Eq) {
11090                        return Err(self.err(alloc::format!(
11091                            "expected '=' after hot_tier_bytes, got {:?}",
11092                            self.peek()
11093                        )));
11094                    }
11095                    self.advance();
11096                    let n = self.expect_u64_literal()?;
11097                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
11098                }
11099                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
11100                // accept-and-no-op for ALTER TABLE SET <subject>
11101                // forms that pg_dump emits but SPG either treats
11102                // as N/A (single-tenant, single-owner, no shared
11103                // tablespaces) or accepts the dump-side declaration
11104                // without runtime effect:
11105                //   SET SCHEMA <name>            (18.11)
11106                //   SET TABLESPACE <name>        (18.8)
11107                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
11108                //   SET WITHOUT CLUSTER          (18.13)
11109                //   SET WITHOUT OIDS             (PG legacy)
11110                //   SET (option = value, …)      (storage parameters)
11111                //   SET REPLICA IDENTITY {…}     (18.14)
11112                if setting.eq_ignore_ascii_case("schema")
11113                    || setting.eq_ignore_ascii_case("tablespace")
11114                    || setting.eq_ignore_ascii_case("logged")
11115                    || setting.eq_ignore_ascii_case("unlogged")
11116                    || setting.eq_ignore_ascii_case("without")
11117                {
11118                    self.consume_until_statement_boundary();
11119                    return Ok(Vec::new());
11120                }
11121                if setting.eq_ignore_ascii_case("replica") {
11122                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
11123                    self.consume_until_statement_boundary();
11124                    return Ok(Vec::new());
11125                }
11126                // SET (option=value, …) — storage parameters.
11127                if matches!(self.peek(), Token::LParen) {
11128                    self.consume_until_statement_boundary();
11129                    return Ok(Vec::new());
11130                }
11131                Err(self.err(alloc::format!(
11132                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
11133                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
11134                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
11135                )))
11136            }
11137            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
11138            // not ignored: round 645 gave SPG the inheritance the
11139            // v7.37.18 no-op said it did not have.
11140            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
11141                self.advance();
11142                let parent = self.expect_ident_like()?;
11143                self.consume_until_statement_boundary();
11144                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11145                    parent,
11146                    detach: false
11147                }])
11148            }
11149            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
11150            // LEVEL SECURITY`, which has its own RLS arm below — without
11151            // the guard this swallowed NO FORCE as a no-op.
11152            Token::Ident(s)
11153                if s.eq_ignore_ascii_case("no")
11154                    && !matches!(
11155                        self.tokens.get(self.pos + 1),
11156                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11157                    ) =>
11158            {
11159                self.advance();
11160                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
11161                    if k.eq_ignore_ascii_case("inherit"))
11162                {
11163                    self.advance();
11164                    let parent = self.expect_ident_like()?;
11165                    self.consume_until_statement_boundary();
11166                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11167                        parent,
11168                        detach: true
11169                    }]);
11170                }
11171                self.consume_until_statement_boundary();
11172                Ok(Vec::new())
11173            }
11174            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
11175            // single-owner, so there is still nothing to record.
11176            //
11177            // v7.39 (round 652) — but the name now reaches the engine,
11178            // which refuses a role that does not exist as PG does. The
11179            // no-op was swallowing the whole statement, so a dump naming
11180            // a role this server never heard of restored clean and left
11181            // the table owned by whoever ran the restore.
11182            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
11183                self.advance();
11184                if matches!(self.peek(), Token::To) {
11185                    self.advance();
11186                }
11187                let role = self.expect_ident_like()?;
11188                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
11189                    role
11190                }])
11191            }
11192            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11193            // PG sets a hint; SPG doesn't have clustered storage, so the
11194            // hint itself stays a no-op.
11195            //
11196            // v7.39 (round 652) — the index name is checked now. PG
11197            // errors on one that does not exist, and swallowing that let
11198            // a typo'd CLUSTER ON pass silently.
11199            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11200                self.advance();
11201                // `ON` is a reserved token, not an ident.
11202                if !matches!(self.peek(), Token::On) {
11203                    return Err(self.err(alloc::format!(
11204                        "expected ON after CLUSTER, got {:?}",
11205                        self.peek()
11206                    )));
11207                }
11208                self.advance();
11209                let index = self.expect_ident_like()?;
11210                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11211                    index: Some(index)
11212                }])
11213            }
11214            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11215            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11216            // what a logical decoder puts in the old-tuple image; SPG's
11217            // replication is SQL-text, so there is nothing to record.
11218            // Accept-and-no-op (it used to be a parse error).
11219            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11220                self.advance();
11221                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11222                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11223                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11224                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11225                {
11226                    self.advance(); // IDENTITY
11227                    self.advance(); // USING
11228                    if matches!(self.peek(), Token::Index)
11229                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11230                    {
11231                        self.advance();
11232                    }
11233                    let index = self.expect_ident_like()?;
11234                    self.consume_until_statement_boundary();
11235                    return Ok(alloc::vec![
11236                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11237                    ]);
11238                }
11239                self.consume_until_statement_boundary();
11240                Ok(Vec::new())
11241            }
11242            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11243            //
11244            // v7.39 (round 652) — it used to consume the statement and
11245            // return nothing, on the stated theory that SPG validated at
11246            // ADD CONSTRAINT time so there was never anything left to
11247            // validate. Measured against PG18, ADD CONSTRAINT did not
11248            // scan the existing rows at all — the comment described a
11249            // property SPG did not have, which is why nobody looked. Both
11250            // halves are real now: ADD scans unless told NOT VALID, and
11251            // this scans what NOT VALID skipped.
11252            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11253                self.advance();
11254                self.expect_keyword_ident("constraint")?;
11255                let name = self.expect_ident_like()?;
11256                Ok(alloc::vec![
11257                    crate::ast::AlterTableTarget::ValidateConstraint { name }
11258                ])
11259            }
11260            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11261            // SET (option = value, …). PG uses it to clear per-table
11262            // storage params like fillfactor or autovacuum_*. SPG
11263            // engine-manages those parameters; accept-and-no-op.
11264            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11265                self.advance();
11266                self.consume_until_statement_boundary();
11267                Ok(Vec::new())
11268            }
11269            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11270            // type-of binding (PG 9.0+). SPG composite types
11271            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11272            // TABLE OF is rare and inverse of CREATE TABLE OF.
11273            // Accept-and-no-op until a customer dump round-trips it.
11274            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11275                self.advance();
11276                // v7.39 (round 710) — the type name is validated now.
11277                let type_name = self.expect_ident_like()?;
11278                self.consume_until_statement_boundary();
11279                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11280                    type_name
11281                }])
11282            }
11283            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11284            // (reserved keyword) rather than Token::Ident("not"),
11285            // so it needs its own arm. Accept-and-no-op same as OF.
11286            Token::Not => {
11287                self.advance();
11288                self.consume_until_statement_boundary();
11289                Ok(Vec::new())
11290            }
11291            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11292            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11293                self.advance();
11294                self.expect_row_level_security()?;
11295                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11296                    enabled: None,
11297                    force: Some(true),
11298                }])
11299            }
11300            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11301            Token::Ident(s)
11302                if s.eq_ignore_ascii_case("no")
11303                    && matches!(
11304                        self.tokens.get(self.pos + 1),
11305                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11306                    ) =>
11307            {
11308                self.advance(); // NO
11309                self.advance(); // FORCE
11310                self.expect_row_level_security()?;
11311                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11312                    enabled: None,
11313                    force: Some(false),
11314                }])
11315            }
11316            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11317            // (sets relrowsecurity). The guard requires the next token to be
11318            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11319            Token::Ident(s)
11320                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11321                    && matches!(
11322                        self.tokens.get(self.pos + 1),
11323                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11324                    ) =>
11325            {
11326                let enabled = s.eq_ignore_ascii_case("enable");
11327                self.advance(); // ENABLE/DISABLE
11328                self.expect_row_level_security()?;
11329                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11330                    enabled: Some(enabled),
11331                    force: None,
11332                }])
11333            }
11334            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11335                self.advance();
11336                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11337                // {INDEX|KEY} [name] (cols)`, which every ORM migration
11338                // emits. The same grammar CREATE TABLE already accepts
11339                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11340                // through the SAME parser — an ALTER-only copy would be a
11341                // second place for the two to drift.
11342                if self.peek_mysql_inline_key_start() {
11343                    return Ok(match self.parse_mysql_inline_key()? {
11344                        Some(c) => {
11345                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11346                        }
11347                        // FULLTEXT / SPATIAL parse and are accepted as a
11348                        // no-op here exactly as they are inline.
11349                        None => Vec::new(),
11350                    });
11351                }
11352                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11353                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11354                // PRIMARY KEY this way; mysqldump emits both.
11355                // Peek-only dispatch (no advance) — `advance()`
11356                // destructively replaces consumed tokens with Eof,
11357                // so saved-pos restore would land on Eofs.
11358                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11359                {
11360                    // The next-but-one ident is the constraint
11361                    // name; the one after THAT is the kind.
11362                    let kind_pos = self.pos + 2;
11363                    let kind = self.tokens.get(kind_pos).cloned();
11364                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11365                    {
11366                        let fk = self.parse_table_level_fk()?;
11367                        return Ok(alloc::vec![
11368                            crate::ast::AlterTableTarget::AddForeignKey(fk)
11369                        ]);
11370                    }
11371                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11372                    {
11373                        self.advance(); // CONSTRAINT
11374                        // v7.39 (read01 round 48) — keep the name; the engine
11375                        // stores it now instead of dropping it on the floor.
11376                        let con_name = self.expect_ident_like()?;
11377                        self.advance(); // PRIMARY
11378                        self.expect_keyword_ident("key")?;
11379                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11380                        // v7.39 (round 711) — the ALTER form carries the
11381                        // timing too (pg_dump writes it here).
11382                        let (deferrable, initially_deferred) =
11383                            self.consume_deferrable_clauses_timed()?;
11384                        return Ok(alloc::vec![
11385                            crate::ast::AlterTableTarget::AddTableConstraint(
11386                                crate::ast::TableConstraint::PrimaryKey {
11387                                    name: Some(con_name),
11388                                    columns: cols,
11389                                    deferrable,
11390                                    initially_deferred,
11391                                }
11392                            )
11393                        ]);
11394                    }
11395                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11396                    {
11397                        self.advance(); // CONSTRAINT
11398                        // v7.39 (read01 round 48) — keep the name.
11399                        let con_name = self.expect_ident_like()?;
11400                        // v7.22 (mailrs round-13 gap 6) — delegate so
11401                        // the optional `NULLS [NOT] DISTINCT` modifier
11402                        // parses here too (pg_dump emits the ALTER
11403                        // form; semantics enforced by the engine
11404                        // since v7.13).
11405                        let mut uc = self.parse_table_level_unique()?;
11406                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11407                            *name = Some(con_name);
11408                        }
11409                        return Ok(alloc::vec![
11410                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11411                        ]);
11412                    }
11413                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11414                    {
11415                        self.advance(); // CONSTRAINT
11416                        // v7.39 (read01 round 48) — keep the name.
11417                        let con_name = self.expect_ident_like()?;
11418                        self.advance(); // CHECK
11419                        if !matches!(self.peek(), Token::LParen) {
11420                            return Err(self.err(alloc::format!(
11421                                "expected '(' after CHECK, got {:?}", self.peek()
11422                            )));
11423                        }
11424                        self.advance();
11425                        let expr = self.parse_expr(0)?;
11426                        if matches!(self.peek(), Token::RParen) {
11427                            self.advance();
11428                        }
11429                        let not_valid = self.parse_not_valid_suffix();
11430                        return Ok(alloc::vec![
11431                            crate::ast::AlterTableTarget::AddTableConstraint(
11432                                crate::ast::TableConstraint::Check {
11433                                    name: Some(con_name),
11434                                    expr,
11435                                    not_valid,
11436                                }
11437                            )
11438                        ]);
11439                    }
11440                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11441                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11442                    // exclusion constraints via this ALTER form.
11443                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11444                    {
11445                        self.advance(); // CONSTRAINT
11446                        let con_name = self.expect_ident_like()?;
11447                        let mut ex = self.parse_table_level_exclude()?;
11448                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11449                            *name = Some(con_name);
11450                        }
11451                        return Ok(alloc::vec![
11452                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11453                        ]);
11454                    }
11455                    // Unknown kind — fall through to FK path which
11456                    // produces a descriptive parse error.
11457                }
11458                let is_fk = matches!(
11459                    self.peek(),
11460                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11461                        || s.eq_ignore_ascii_case("foreign")
11462                );
11463                if is_fk {
11464                    let fk = self.parse_table_level_fk()?;
11465                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11466                }
11467                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11468                // (no CONSTRAINT prefix) — same dispatch.
11469                match self.peek().clone() {
11470                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11471                        self.advance();
11472                        self.expect_keyword_ident("key")?;
11473                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11474                        let (deferrable, initially_deferred) =
11475                            self.consume_deferrable_clauses_timed()?;
11476                        return Ok(alloc::vec![
11477                            crate::ast::AlterTableTarget::AddTableConstraint(
11478                                crate::ast::TableConstraint::PrimaryKey {
11479                                    name: None,
11480                                    columns: cols,
11481                                    deferrable,
11482                                    initially_deferred,
11483                                }
11484                            )
11485                        ]);
11486                    }
11487                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11488                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
11489                        let uc = self.parse_table_level_unique()?;
11490                        return Ok(alloc::vec![
11491                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11492                        ]);
11493                    }
11494                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11495                    // prefix). The other three bare forms were here and
11496                    // this one was not, so it fell through to the column
11497                    // path and came back as "unexpected reserved keyword
11498                    // 'check' at start of column definition".
11499                    _ if self.peek_table_level_check_start() => {
11500                        let chk = self.parse_table_level_check()?;
11501                        let not_valid = self.parse_not_valid_suffix();
11502                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11503                            unreachable!("parse_table_level_check returns Check")
11504                        };
11505                        return Ok(alloc::vec![
11506                            crate::ast::AlterTableTarget::AddTableConstraint(
11507                                crate::ast::TableConstraint::Check {
11508                                    name: None,
11509                                    expr,
11510                                    not_valid,
11511                                }
11512                            )
11513                        ]);
11514                    }
11515                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11516                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11517                        let ex = self.parse_table_level_exclude()?;
11518                        return Ok(alloc::vec![
11519                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11520                        ]);
11521                    }
11522                    _ => {}
11523                }
11524                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11525                    self.advance();
11526                }
11527                let mut if_not_exists = false;
11528                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11529                    self.advance();
11530                    if !matches!(self.peek(), Token::Not) {
11531                        return Err(self.err(alloc::format!(
11532                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11533                            self.peek()
11534                        )));
11535                    }
11536                    self.advance();
11537                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11538                        return Err(self.err(alloc::format!(
11539                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11540                            self.peek()
11541                        )));
11542                    }
11543                    self.advance();
11544                    if_not_exists = true;
11545                }
11546                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11547                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11548                // returns ColumnDef + an optional inline FK.
11549                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11550                let col_name = column.name.clone();
11551                // v7.39.9 — MySQL says where the column goes.
11552                let position = self.parse_column_position();
11553                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11554                    column,
11555                    if_not_exists,
11556                    position,
11557                }];
11558                if let Some(mut fk) = col_level_fk {
11559                    if fk.columns.is_empty() {
11560                        fk.columns.push(col_name);
11561                    }
11562                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11563                }
11564                Ok(out)
11565            }
11566            Token::Drop => {
11567                self.advance();
11568                // v7.13.3 — dispatch on the next token. mailrs round-7
11569                // S8 closed DROP COLUMN; round-6 S7 closed
11570                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11571                // RESTRICT modifiers.
11572                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11573                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11574                let subject = match self.peek() {
11575                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11576                        self.advance();
11577                        "constraint"
11578                    }
11579                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11580                        self.advance();
11581                        "column"
11582                    }
11583                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11584                    // `INDEX` lexes as the reserved Token::Index, so it is
11585                    // unambiguous. `KEY` is a plain ident, and PG allows a
11586                    // column literally named "key", so only read it as the
11587                    // keyword when a name follows it.
11588                    Token::Index => {
11589                        self.advance();
11590                        "index"
11591                    }
11592                    Token::Ident(s)
11593                        if s.eq_ignore_ascii_case("key")
11594                            && matches!(
11595                                self.tokens.get(self.pos + 1),
11596                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11597                            ) =>
11598                    {
11599                        self.advance();
11600                        "index"
11601                    }
11602                    // PG-canonical bare `DROP <col>` without COLUMN
11603                    // keyword is also valid; treat any other ident
11604                    // as the column name.
11605                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11606                    other => {
11607                        return Err(self.err(alloc::format!(
11608                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11609                        )));
11610                    }
11611                };
11612                let mut if_exists = false;
11613                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11614                    let n1 = self.tokens.get(self.pos + 1);
11615                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11616                        self.advance();
11617                        self.advance();
11618                        if_exists = true;
11619                    }
11620                }
11621                let name = self.expect_ident_like()?;
11622                let mut cascade = false;
11623                if matches!(
11624                    self.peek(),
11625                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11626                        || s.eq_ignore_ascii_case("restrict")
11627                ) {
11628                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11629                    {
11630                        cascade = true;
11631                    }
11632                    self.advance();
11633                }
11634                if subject == "index" {
11635                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11636                        name,
11637                        if_exists,
11638                    }])
11639                } else if subject == "constraint" {
11640                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11641                        name,
11642                        if_exists,
11643                    }])
11644                } else {
11645                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11646                        column: name,
11647                        if_exists,
11648                        cascade,
11649                    }])
11650                }
11651            }
11652            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11653                self.advance();
11654                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11655                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11656                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11657                // immediately; accept-and-no-op.
11658                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11659                    self.advance();
11660                    self.consume_until_statement_boundary();
11661                    return Ok(Vec::new());
11662                }
11663                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11664                    self.advance();
11665                }
11666                let col_name = self.expect_ident_like()?;
11667                match self.peek() {
11668                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11669                        self.advance();
11670                    }
11671                    // v7.14.0 — pg_dump emits BIGSERIAL via
11672                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11673                    // nextval('seq')` (the sequence is created
11674                    // separately). SPG's BIGSERIAL already uses
11675                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11676                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11677                    // engine no-ops by consuming the tail.
11678                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11679                        // v7.22 (round-13 T2) — `SET DEFAULT
11680                        // nextval('…')` is how pg_dump spells a
11681                        // SERIAL column (plain integer in CREATE
11682                        // TABLE + this ALTER). It used to be
11683                        // swallowed as a no-op, which silently
11684                        // STRIPPED auto-increment from imported
11685                        // schemas — the first post-import INSERT
11686                        // without an explicit id then violated NOT
11687                        // NULL. Lower it to the auto-increment
11688                        // marker instead.
11689                        let is_default_nextval =
11690                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11691                                && matches!(
11692                                    self.tokens.get(self.pos + 2),
11693                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11694                                );
11695                        if is_default_nextval {
11696                            let seq_name = self.scan_sequence_name_until_boundary();
11697                            return Ok(alloc::vec![
11698                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11699                                    column: col_name,
11700                                    seq_name,
11701                                }
11702                            ]);
11703                        }
11704                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11705                        self.advance(); // consume "set"
11706                        match self.peek().clone() {
11707                            Token::Default => {
11708                                self.advance();
11709                                let default_expr = self.parse_expr(0)?;
11710                                return Ok(alloc::vec![
11711                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11712                                        column: col_name,
11713                                        default_expr,
11714                                    }
11715                                ]);
11716                            }
11717                            Token::Not => {
11718                                self.advance();
11719                                if !matches!(self.peek(), Token::Null) {
11720                                    return Err(self.err(alloc::format!(
11721                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11722                                        self.peek()
11723                                    )));
11724                                }
11725                                self.advance();
11726                                return Ok(alloc::vec![
11727                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11728                                        column: col_name,
11729                                    }
11730                                ]);
11731                            }
11732                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11733                            // stored generated column's expression and
11734                            // recompute existing rows.
11735                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11736                                self.advance(); // EXPRESSION
11737                                if matches!(self.peek(), Token::As) {
11738                                    self.advance();
11739                                }
11740                                let expr = self.parse_expr(0)?;
11741                                return Ok(alloc::vec![
11742                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11743                                        column: col_name,
11744                                        expr,
11745                                    }
11746                                ]);
11747                            }
11748                            other => {
11749                                // Other SET subjects (STATISTICS,
11750                                // STORAGE, COMPRESSION, …) stay no-ops —
11751                                // storage hints with no SPG semantics.
11752                                let _ = other;
11753                                self.consume_until_statement_boundary();
11754                                return Ok(Vec::new());
11755                            }
11756                        }
11757                    }
11758                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11759                        self.advance(); // consume "drop"
11760                        return self.parse_alter_column_drop_tail(col_name);
11761                    }
11762                    Token::Drop => {
11763                        self.advance(); // consume Drop token
11764                        return self.parse_alter_column_drop_tail(col_name);
11765                    }
11766                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11767                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11768                        // GENERATED { ALWAYS | BY DEFAULT } AS
11769                        // IDENTITY ( … )`: pg_dump's spelling for
11770                        // identity columns. Same auto-increment
11771                        // lowering as the nextval default; the
11772                        // sequence options inside the parens are
11773                        // no-ops under SPG's max+1 semantics.
11774                        let is_generated = matches!(
11775                            self.tokens.get(self.pos + 1),
11776                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11777                        );
11778                        if !is_generated {
11779                            return Err(self.err(alloc::format!(
11780                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11781                                self.tokens.get(self.pos + 1)
11782                            )));
11783                        }
11784                        let seq_name = self.scan_sequence_name_until_boundary();
11785                        return Ok(alloc::vec![
11786                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11787                                column: col_name,
11788                                seq_name,
11789                            }
11790                        ]);
11791                    }
11792                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11793                    // column: floor the next allocated value at n (bare
11794                    // RESTART = restart from the start value, 1).
11795                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11796                        self.advance();
11797                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11798                        {
11799                            self.advance();
11800                            let neg = if matches!(self.peek(), Token::Minus) {
11801                                self.advance();
11802                                true
11803                            } else {
11804                                false
11805                            };
11806                            match self.advance() {
11807                                Token::Integer(v) => Some(if neg { -v } else { v }),
11808                                other => {
11809                                    return Err(self.err(alloc::format!(
11810                                        "expected integer after RESTART WITH, got {other:?}"
11811                                    )));
11812                                }
11813                            }
11814                        } else {
11815                            None
11816                        };
11817                        return Ok(alloc::vec![
11818                            crate::ast::AlterTableTarget::AlterColumnRestart {
11819                                column: col_name,
11820                                with,
11821                            }
11822                        ]);
11823                    }
11824                    other => {
11825                        return Err(self.err(alloc::format!(
11826                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11827                        )));
11828                    }
11829                }
11830                // v7.39 (round 713) — the type parser has consumed a
11831                // trailing `COLLATE <name>` since Phase 2.5, and
11832                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11833                // TYPE text COLLATE "C"` parsed clean and changed
11834                // nothing. Keep the clause; the engine re-collates.
11835                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11836                    self.parse_type_with_implied_flags()?;
11837                let collation = if coll_explicit {
11838                    coll_name.map(|n| (coll, n))
11839                } else {
11840                    None
11841                };
11842                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11843                {
11844                    self.advance();
11845                    Some(self.parse_expr(0)?)
11846                } else {
11847                    None
11848                };
11849                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11850                    column: col_name,
11851                    new_type,
11852                    using,
11853                    collation,
11854                }])
11855            }
11856            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11857            // PG also supports `RENAME TO new_table` for table-name
11858            // rename; that surface is deferred (pg_dump never emits
11859            // it). If the first post-RENAME ident is `TO`, the user
11860            // is asking for table rename — error with a clear
11861            // message rather than misparsing `TO` as a column name.
11862            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11863                self.advance();
11864                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11865                // table-name rename (mailrs round-10 A.5 — used
11866                // by migrate-042's `RENAME TO email_contacts`).
11867                // `TO` lexes as Token::To.
11868                if matches!(self.peek(), Token::To)
11869                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11870                {
11871                    self.advance();
11872                    let new = self.expect_ident_like()?;
11873                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11874                        new,
11875                    }]);
11876                }
11877                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11878                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11879                    self.advance();
11880                    let old = self.expect_ident_like()?;
11881                    if matches!(self.peek(), Token::To) {
11882                        self.advance();
11883                    } else {
11884                        self.expect_keyword_ident("to")?;
11885                    }
11886                    let new = self.expect_ident_like()?;
11887                    return Ok(alloc::vec![
11888                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11889                    ]);
11890                }
11891                // v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
11892                // PostgreSQL renames an index with its own top-level
11893                // `ALTER INDEX`, so this spelling had nowhere to go and
11894                // answered 1064; MySQL 9.7.2 parses it and answers 1176
11895                // when the key is not there.
11896                if matches!(self.peek(), Token::Index)
11897                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key"))
11898                {
11899                    self.advance();
11900                    let old = self.expect_ident_like()?;
11901                    if matches!(self.peek(), Token::To) {
11902                        self.advance();
11903                    } else {
11904                        self.expect_keyword_ident("to")?;
11905                    }
11906                    let new = self.expect_ident_like()?;
11907                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameIndex {
11908                        old,
11909                        new,
11910                    }]);
11911                }
11912                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11913                    self.advance();
11914                }
11915                let old = self.expect_ident_like()?;
11916                // `TO` is a reserved keyword token; accept both
11917                // Token::To and Token::Ident("to") for consistency.
11918                if matches!(self.peek(), Token::To) {
11919                    self.advance();
11920                } else {
11921                    self.expect_keyword_ident("to")?;
11922                }
11923                let new = self.expect_ident_like()?;
11924                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11925                    old,
11926                    new,
11927                }])
11928            }
11929            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11930            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11931            // every data block with these. Real disable semantics —
11932            // not no-op — because reload correctness assumes the
11933            // triggers don't fire (rows already carry their
11934            // computed values from prod).
11935            Token::Ident(s)
11936                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11937            {
11938                let enabled = s.eq_ignore_ascii_case("enable");
11939                self.advance();
11940                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11941                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11942                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11943                // pg_dump output) — anything else falls through to
11944                // the catch-all error below.
11945                // v7.22 (round-13 T3) — mysqldump wraps every data
11946                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11947                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11948                // maintains indexes incrementally — engine no-op.
11949                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11950                    self.advance();
11951                    return Ok(Vec::new());
11952                }
11953                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11954                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11955                // to gate triggers on session_replication_role; SPG
11956                // has no replica role, so the prefix is consumed and
11957                // treated identically to the plain ENABLE/DISABLE
11958                // TRIGGER form.
11959                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11960                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11961                {
11962                    self.advance();
11963                }
11964                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11965                    return Err(self.err(alloc::format!(
11966                        "expected TRIGGER after {}, got {:?}",
11967                        if enabled { "ENABLE" } else { "DISABLE" },
11968                        self.peek()
11969                    )));
11970                }
11971                self.advance();
11972                // `ALL` lexes as Token::All (reserved); also
11973                // accept Token::Ident("all") for symmetry.
11974                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
11975                // TRIGGER selectors. USER (= all user triggers) is
11976                // semantically ALL here; REPLICA / ALWAYS gate on
11977                // session_replication_role which SPG doesn't track.
11978                // All map to TriggerSelector::All.
11979                let which = if matches!(self.peek(), Token::All)
11980                    || matches!(self.peek(), Token::Ident(s)
11981                        if s.eq_ignore_ascii_case("all")
11982                            || s.eq_ignore_ascii_case("user")
11983                            || s.eq_ignore_ascii_case("replica")
11984                            || s.eq_ignore_ascii_case("always"))
11985                {
11986                    self.advance();
11987                    crate::ast::TriggerSelector::All
11988                } else {
11989                    let name = self.expect_ident_like()?;
11990                    crate::ast::TriggerSelector::Named(name)
11991                };
11992                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
11993                    which,
11994                    enabled,
11995                }])
11996            }
11997            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
11998            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
11999                self.advance();
12000                if !matches!(self.peek(), Token::Partition)
12001                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12002                        if s.eq_ignore_ascii_case("partition"))
12003                {
12004                    return Err(self.err(alloc::format!(
12005                        "expected PARTITION after ATTACH, got {:?}",
12006                        self.peek()
12007                    )));
12008                }
12009                self.advance();
12010                let child = self.expect_ident_like()?;
12011                let bounds = self.parse_partition_bounds_tail()?;
12012                Ok(alloc::vec![
12013                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
12014                ])
12015            }
12016            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
12017            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
12018                self.advance();
12019                if !matches!(self.peek(), Token::Partition)
12020                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12021                        if s.eq_ignore_ascii_case("partition"))
12022                {
12023                    return Err(self.err(alloc::format!(
12024                        "expected PARTITION after DETACH, got {:?}",
12025                        self.peek()
12026                    )));
12027                }
12028                self.advance();
12029                let child = self.expect_ident_like()?;
12030                let mut concurrently = false;
12031                let mut finalize = false;
12032                loop {
12033                    match self.peek().clone() {
12034                        Token::Ident(s) | Token::QuotedIdent(s)
12035                            if s.eq_ignore_ascii_case("concurrently") =>
12036                        {
12037                            self.advance();
12038                            concurrently = true;
12039                        }
12040                        Token::Ident(s) | Token::QuotedIdent(s)
12041                            if s.eq_ignore_ascii_case("finalize") =>
12042                        {
12043                            self.advance();
12044                            finalize = true;
12045                        }
12046                        _ => break,
12047                    }
12048                }
12049                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
12050                    child,
12051                    concurrently,
12052                    finalize,
12053                }])
12054            }
12055            other => Err(self.err(alloc::format!(
12056                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
12057            ))),
12058        }
12059    }
12060
12061    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
12062    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
12063    /// TABLE … ATTACH PARTITION. Shares the same grammar as
12064    /// `parse_partition_of_tail`'s bounds branch.
12065    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
12066    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
12067    /// lowering each to the respective AlterTableTarget. Any
12068    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
12069    /// no-op via consume_until_statement_boundary.
12070    fn parse_alter_column_drop_tail(
12071        &mut self,
12072        col_name: String,
12073    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
12074        match self.peek().clone() {
12075            Token::Default => {
12076                self.advance();
12077                Ok(alloc::vec![
12078                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
12079                ])
12080            }
12081            Token::Not => {
12082                self.advance();
12083                if !matches!(self.peek(), Token::Null) {
12084                    return Err(self.err(alloc::format!(
12085                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
12086                        self.peek()
12087                    )));
12088                }
12089                self.advance();
12090                Ok(alloc::vec![
12091                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
12092                ])
12093            }
12094            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
12095            // generated column into a plain column.
12096            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
12097                self.advance();
12098                // v7.39 (round 187, U10) — IF EXISTS was consumed but
12099                // dropped, so the engine still errored on a plain
12100                // column; PG's semantics are NOTICE + skip.
12101                let mut if_exists = false;
12102                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12103                    self.advance();
12104                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12105                        self.advance();
12106                        if_exists = true;
12107                    }
12108                }
12109                Ok(alloc::vec![
12110                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
12111                        column: col_name,
12112                        if_exists,
12113                    }
12114                ])
12115            }
12116            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
12117            // identity column into a plain column.
12118            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
12119                self.advance();
12120                let mut if_exists = false;
12121                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12122                    self.advance();
12123                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12124                        self.advance();
12125                        if_exists = true;
12126                    }
12127                }
12128                Ok(alloc::vec![
12129                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
12130                        column: col_name,
12131                        if_exists,
12132                    }
12133                ])
12134            }
12135            _ => {
12136                self.consume_until_statement_boundary();
12137                Ok(Vec::new())
12138            }
12139        }
12140    }
12141
12142    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
12143    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
12144    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
12145    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
12146    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
12147        let mut opts = crate::ast::CopyOptions::default();
12148        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
12149            return Ok(opts);
12150        }
12151        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
12152            self.advance();
12153        }
12154        if matches!(self.peek(), Token::LParen) {
12155            self.advance();
12156            loop {
12157                self.parse_one_copy_option(&mut opts)?;
12158                match self.peek() {
12159                    Token::Comma => {
12160                        self.advance();
12161                    }
12162                    Token::RParen => {
12163                        self.advance();
12164                        break;
12165                    }
12166                    other => {
12167                        return Err(self.err(alloc::format!(
12168                            "expected ',' or ')' in COPY options, got {other:?}"
12169                        )));
12170                    }
12171                }
12172            }
12173        } else {
12174            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12175                self.parse_one_copy_option(&mut opts)?;
12176            }
12177        }
12178        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12179            return Err(self.err(alloc::format!(
12180                "unexpected token after COPY options: {:?}",
12181                self.peek()
12182            )));
12183        }
12184        Ok(opts)
12185    }
12186
12187    fn parse_one_copy_option(
12188        &mut self,
12189        opts: &mut crate::ast::CopyOptions,
12190    ) -> Result<(), ParseError> {
12191        use crate::ast::CopyFormat;
12192        // The option keyword. NULL lexes as its own token; the rest are
12193        // bare identifiers.
12194        let kw = match self.advance() {
12195            Token::Null => alloc::string::String::from("NULL"),
12196            Token::Ident(s) => s.to_uppercase(),
12197            other => {
12198                return Err(self.err(alloc::format!(
12199                    "expected a COPY option keyword, got {other:?}"
12200                )));
12201            }
12202        };
12203        match kw.as_str() {
12204            "FORMAT" => {
12205                let fmt = self.expect_ident_like()?;
12206                match fmt.to_ascii_uppercase().as_str() {
12207                    "CSV" => opts.format = CopyFormat::Csv,
12208                    "TEXT" => opts.format = CopyFormat::Text,
12209                    other => {
12210                        return Err(self.err(alloc::format!(
12211                            "COPY format \"{}\" not recognized",
12212                            other.to_ascii_lowercase()
12213                        )));
12214                    }
12215                }
12216            }
12217            // Legacy bare format keywords.
12218            "CSV" => opts.format = CopyFormat::Csv,
12219            "TEXT" => opts.format = CopyFormat::Text,
12220            "HEADER" => {
12221                opts.header = match self.peek() {
12222                    Token::True => {
12223                        self.advance();
12224                        true
12225                    }
12226                    Token::False => {
12227                        self.advance();
12228                        false
12229                    }
12230                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12231                        self.advance();
12232                        true
12233                    }
12234                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12235                        self.advance();
12236                        false
12237                    }
12238                    // Bare HEADER (no boolean) means HEADER true.
12239                    _ => true,
12240                };
12241            }
12242            // r1066 (7.38 S5.1) — pgbench 14+ loads with
12243            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12244            // vacuum bookkeeping on a freshly created/truncated
12245            // table; SPG's per-statement visibility makes it a
12246            // faithful no-op, and rejecting it aborted `pgbench -i`
12247            // against the drop-in. Accept ON/OFF/bare, change nothing.
12248            "FREEZE" => match self.peek() {
12249                Token::True | Token::False => {
12250                    self.advance();
12251                }
12252                Token::Ident(s)
12253                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12254                {
12255                    self.advance();
12256                }
12257                _ => {}
12258            },
12259            "DELIMITER" | "QUOTE" | "ESCAPE" => {
12260                let s = match self.advance() {
12261                    Token::String(s) => s,
12262                    other => {
12263                        return Err(self.err(alloc::format!(
12264                            "COPY {kw} expects a single-character string, got {other:?}"
12265                        )));
12266                    }
12267                };
12268                // v7.39 (round 247) — PG's wording (0A000), keyword in
12269                // lowercase: "COPY delimiter must be a single one-byte
12270                // character".
12271                let one_byte_err = || {
12272                    self.err(alloc::format!(
12273                        "COPY {} must be a single one-byte character",
12274                        kw.to_ascii_lowercase()
12275                    ))
12276                };
12277                let mut chars = s.chars();
12278                let c = chars.next().ok_or_else(one_byte_err)?;
12279                if chars.next().is_some() || c.len_utf8() != 1 {
12280                    return Err(one_byte_err());
12281                }
12282                match kw.as_str() {
12283                    "DELIMITER" => opts.delimiter = Some(c),
12284                    "QUOTE" => opts.quote = Some(c),
12285                    _ => opts.escape = Some(c),
12286                }
12287            }
12288            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12289            "FORCE_QUOTE" => {
12290                if matches!(self.peek(), Token::Star) {
12291                    self.advance();
12292                    opts.force_quote = Some(Vec::new());
12293                } else {
12294                    if !matches!(self.peek(), Token::LParen) {
12295                        return Err(self.err(alloc::format!(
12296                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12297                            self.peek()
12298                        )));
12299                    }
12300                    self.advance();
12301                    let mut cols = Vec::new();
12302                    loop {
12303                        cols.push(self.expect_ident_like()?);
12304                        match self.peek() {
12305                            Token::Comma => {
12306                                self.advance();
12307                            }
12308                            Token::RParen => {
12309                                self.advance();
12310                                break;
12311                            }
12312                            other => {
12313                                return Err(self.err(alloc::format!(
12314                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12315                                )));
12316                            }
12317                        }
12318                    }
12319                    opts.force_quote = Some(cols);
12320                }
12321            }
12322            "NULL" => {
12323                opts.null_str = Some(match self.advance() {
12324                    Token::String(s) => s,
12325                    other => {
12326                        return Err(self.err(alloc::format!(
12327                            "COPY NULL expects a quoted string, got {other:?}"
12328                        )));
12329                    }
12330                });
12331            }
12332            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12333            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12334            // FORCE_NULL too.
12335            "FORCE_NOT_NULL" | "FORCE_NULL" => {
12336                let cols = self.parse_copy_column_list(&kw)?;
12337                if kw == "FORCE_NOT_NULL" {
12338                    opts.force_not_null = Some(cols);
12339                } else {
12340                    opts.force_null = Some(cols);
12341                }
12342            }
12343            other => {
12344                // PG's wording, lowercased option name.
12345                return Err(self.err(alloc::format!(
12346                    "option \"{}\" not recognized",
12347                    other.to_ascii_lowercase()
12348                )));
12349            }
12350        }
12351        Ok(())
12352    }
12353
12354    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12355    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12356    /// is the `*` spelling.
12357    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12358        if matches!(self.peek(), Token::Star) {
12359            self.advance();
12360            return Ok(Vec::new());
12361        }
12362        if !matches!(self.peek(), Token::LParen) {
12363            return Err(self.err(alloc::format!(
12364                "expected '(' or '*' after {kw}, got {:?}",
12365                self.peek()
12366            )));
12367        }
12368        self.advance();
12369        let mut cols = Vec::new();
12370        loop {
12371            cols.push(self.expect_ident_like()?);
12372            match self.peek() {
12373                Token::Comma => {
12374                    self.advance();
12375                }
12376                Token::RParen => {
12377                    self.advance();
12378                    break;
12379                }
12380                other => {
12381                    return Err(self.err(alloc::format!(
12382                        "expected ',' or ')' in {kw} list, got {other:?}"
12383                    )));
12384                }
12385            }
12386        }
12387        Ok(cols)
12388    }
12389
12390    fn parse_partition_bounds_tail(
12391        &mut self,
12392    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12393        use crate::ast::PartitionOfBoundsAst;
12394        match self.peek() {
12395            Token::Default => {
12396                self.advance();
12397                Ok(PartitionOfBoundsAst::Default)
12398            }
12399            Token::For => {
12400                self.advance();
12401                if !matches!(self.peek(), Token::Values) {
12402                    return Err(
12403                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12404                    );
12405                }
12406                self.advance();
12407                let want_with = matches!(
12408                    self.peek(),
12409                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12410                );
12411                if want_with {
12412                    self.advance();
12413                    if !matches!(self.peek(), Token::LParen) {
12414                        return Err(self.err(format!(
12415                            "expected '(' after FOR VALUES WITH, got {:?}",
12416                            self.peek()
12417                        )));
12418                    }
12419                    self.advance();
12420                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12421                    loop {
12422                        let key = self.expect_ident_like()?;
12423                        let n = match self.peek().clone() {
12424                            Token::Integer(v) if u32::try_from(v).is_ok() => {
12425                                self.advance();
12426                                v as u32
12427                            }
12428                            other => {
12429                                return Err(self.err(format!(
12430                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12431                                )));
12432                            }
12433                        };
12434                        match key.to_ascii_uppercase().as_str() {
12435                            "MODULUS" => modulus = Some(n),
12436                            "REMAINDER" => remainder = Some(n),
12437                            other => {
12438                                return Err(self.err(format!(
12439                                    "FOR VALUES WITH: unknown key {other:?}; \
12440                                     expected MODULUS or REMAINDER"
12441                                )));
12442                            }
12443                        }
12444                        match self.peek() {
12445                            Token::Comma => {
12446                                self.advance();
12447                            }
12448                            Token::RParen => {
12449                                self.advance();
12450                                break;
12451                            }
12452                            other => {
12453                                return Err(self.err(format!(
12454                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12455                                )));
12456                            }
12457                        }
12458                    }
12459                    let modulus = modulus
12460                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12461                    let remainder = remainder.ok_or_else(|| {
12462                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12463                    })?;
12464                    if modulus == 0 {
12465                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12466                    }
12467                    if remainder >= modulus {
12468                        return Err(self.err(format!(
12469                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12470                        )));
12471                    }
12472                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12473                }
12474                match self.peek() {
12475                    Token::From => {
12476                        self.advance();
12477                        let lower = Box::new(self.parse_partition_bound_expr()?);
12478                        if !matches!(self.peek(), Token::To) {
12479                            return Err(self.err(format!(
12480                                "expected TO after FROM (...), got {:?}",
12481                                self.peek()
12482                            )));
12483                        }
12484                        self.advance();
12485                        let upper = Box::new(self.parse_partition_bound_expr()?);
12486                        Ok(PartitionOfBoundsAst::Range { lower, upper })
12487                    }
12488                    Token::In => {
12489                        self.advance();
12490                        if !matches!(self.peek(), Token::LParen) {
12491                            return Err(self.err(format!(
12492                                "expected '(' after FOR VALUES IN, got {:?}",
12493                                self.peek()
12494                            )));
12495                        }
12496                        self.advance();
12497                        let mut values = Vec::new();
12498                        loop {
12499                            values.push(self.parse_expr(0)?);
12500                            match self.peek() {
12501                                Token::Comma => {
12502                                    self.advance();
12503                                }
12504                                Token::RParen => {
12505                                    self.advance();
12506                                    break;
12507                                }
12508                                other => {
12509                                    return Err(self.err(format!(
12510                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12511                                    )));
12512                                }
12513                            }
12514                        }
12515                        if values.is_empty() {
12516                            return Err(
12517                                self.err("FOR VALUES IN requires at least one literal".to_string())
12518                            );
12519                        }
12520                        Ok(PartitionOfBoundsAst::List { values })
12521                    }
12522                    other => Err(self.err(format!(
12523                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12524                    ))),
12525                }
12526            }
12527            other => Err(self.err(format!(
12528                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12529            ))),
12530        }
12531    }
12532
12533    /// v7.16.2 — peek for `information_schema.<tbl>` /
12534    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12535    /// three tokens + return a synthetic table name the engine's
12536    /// SELECT path recognises as a virtual view. Returns `None`
12537    /// when the head doesn't look like a meta-qualified name.
12538    /// Used by `parse_table_ref` to bypass the
12539    /// `expect_ident_like` schema-strip for these specific PG
12540    /// meta schemas (mailrs round-10 A.3).
12541    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12542        // Extract the schema name. Must be a plain ident token.
12543        let schema = match self.tokens.get(self.pos) {
12544            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12545            _ => return None,
12546        };
12547        // Dot.
12548        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12549            return None;
12550        }
12551        // The table-side ident may lex as a reserved keyword
12552        // (e.g. `Token::Tables`). Tolerate the common ones via a
12553        // helper that reads the trailing token's underlying name.
12554        let tbl = match self.tokens.get(self.pos + 2)? {
12555            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12556            Token::Tables => "tables".to_string(),
12557            // Other PG meta table names that may collide with
12558            // reserved keywords land here as needed.
12559            _ => return None,
12560        };
12561        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12562        // names so the synthetic name doesn't double-prefix
12563        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12564        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12565            ("__spg_info_", tbl.to_ascii_lowercase())
12566        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12567            // v7.39 (round 541) — only the catalogs SPG actually
12568            // synthesises are rewritten, which is what the BARE path
12569            // has always checked. Anything else keeps its own name and
12570            // takes the ordinary route: `pg_stat_activity` and friends
12571            // resolve through meta_view_result, and a name that is no
12572            // catalog at all gets PG's "relation does not exist"
12573            // instead of a message about a view SPG cannot materialise.
12574            let lowered = tbl.to_ascii_lowercase();
12575            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12576                self.advance(); // schema
12577                self.advance(); // dot
12578                self.advance(); // tbl
12579                return Some((lowered.clone(), lowered));
12580            }
12581            let bare = lowered
12582                .strip_prefix("pg_")
12583                .map(alloc::string::String::from)
12584                .unwrap_or(lowered);
12585            ("__spg_pg_", bare)
12586        } else if schema.eq_ignore_ascii_case("mysql") {
12587            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12588            // (`mysql.user`, `mysql.db`). Same synthetic-name
12589            // shape as pg_catalog.
12590            ("__spg_mysql_", tbl.to_ascii_lowercase())
12591        } else {
12592            return None;
12593        };
12594        self.advance(); // schema
12595        self.advance(); // dot
12596        self.advance(); // tbl
12597        Some((
12598            alloc::format!("{prefix}{normalised}"),
12599            tbl.to_ascii_lowercase(),
12600        ))
12601    }
12602
12603    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12604    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12605    /// implicit front of every search_path, so a bare reference to a
12606    /// known catalog table always means the catalog table. Only the
12607    /// names the engine actually synthesises are recognised — any
12608    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12609    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12610        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12611        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12612        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12613        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12614        // through the meta_view_result path instead, and already resolve
12615        // bare — they must NOT be listed here or the __spg_ rewrite would
12616        // mis-target them.)
12617        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12618        let name = match self.tokens.get(self.pos) {
12619            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12620            _ => return None,
12621        };
12622        // A following dot means this ident is a schema qualifier,
12623        // not a table name — let the qualified path handle it.
12624        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12625            return None;
12626        }
12627        if !PG_META_TABLES.contains(&name.as_str()) {
12628            return None;
12629        }
12630        self.advance();
12631        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12632        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12633    }
12634
12635    /// Consume a bare ident if its lowercase matches `kw`, else err.
12636    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12637    /// Peeks only; the caller advances.
12638    fn peek_keyword_ident(&self, kw: &str) -> bool {
12639        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12640    }
12641
12642    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12643        match self.advance() {
12644            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12645            other => Err(ParseError {
12646                message: format!("expected {kw:?}, got {other:?}"),
12647                token_pos: self.consumed_pos(),
12648            }),
12649        }
12650    }
12651
12652    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12653    /// literal (`'foo'`) — same shape used by CREATE USER for the
12654    /// username slot.
12655    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12656        match self.advance() {
12657            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12658            other => Err(ParseError {
12659                message: format!("expected identifier or string, got {other:?}"),
12660                token_pos: self.consumed_pos(),
12661            }),
12662        }
12663    }
12664
12665    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12666        match self.advance() {
12667            Token::String(s) => Ok(s),
12668            other => Err(ParseError {
12669                message: format!("expected quoted string, got {other:?}"),
12670                token_pos: self.consumed_pos(),
12671            }),
12672        }
12673    }
12674
12675    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12676        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12677        // subqueries recurse through here without passing
12678        // parse_expr; share the same nesting budget.
12679        self.enter_nested()?;
12680        let r = self.parse_select_stmt_inner();
12681        self.nest_depth -= 1;
12682        r
12683    }
12684
12685    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12686        // Caller dispatches on Token::Select; the inner helper handles
12687        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12688        // get a fresh bare-select parse and may not have their own ORDER
12689        // BY / LIMIT.
12690        let mut head = self.parse_bare_select()?;
12691        let into = self.pending_select_into.take();
12692        self.parse_setop_chain_into(&mut head)?;
12693        self.parse_select_tail_into(&mut head)?;
12694        // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12695        // `CREATE TABLE t AS SELECT …`, which is what a comment in
12696        // `ast.rs` has claimed since v7.38 and what only CTAS actually
12697        // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12698        // to the body, as it does in PostgreSQL.
12699        if let Some((name, temporary)) = into {
12700            return Ok(Statement::CreateMaterializedView(
12701                crate::ast::CreateMaterializedViewStatement {
12702                    temporary,
12703                    name,
12704                    if_not_exists: false,
12705                    columns: Vec::new(),
12706                    body: head,
12707                    with_data: true,
12708                    as_plain_table: true,
12709                },
12710            ));
12711        }
12712        Ok(Statement::Select(head))
12713    }
12714
12715    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12716    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12717    /// token), and INTERSECT [ALL] (a bare ident — it was never
12718    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12719    /// tighter than UNION / EXCEPT — the executor folds the chain
12720    /// left-to-right, which is already correct for LEADING
12721    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12722    /// pair nests into that previous peer, so A UNION B INTERSECT C
12723    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12724    /// groups.
12725    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12726        // A parenthesized group arrives with its own (already
12727        // regrouped) unions on `head`; only the pairs THIS chain
12728        // appends participate in the precedence regroup below —
12729        // nesting an outer INTERSECT into a group-internal peer
12730        // would dissolve the explicit grouping.
12731        let boundary = head.unions.len();
12732        loop {
12733            let base = match self.peek() {
12734                Token::Union => UnionKind::Distinct,
12735                Token::Except => UnionKind::Except,
12736                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12737                _ => break,
12738            };
12739            self.advance();
12740            let kind = if matches!(self.peek(), Token::All) {
12741                self.advance();
12742                match base {
12743                    UnionKind::Distinct => UnionKind::All,
12744                    UnionKind::Except => UnionKind::ExceptAll,
12745                    _ => UnionKind::IntersectAll,
12746                }
12747            } else {
12748                base
12749            };
12750            let peer = self.parse_bare_select()?;
12751            head.unions.push((kind, peer));
12752        }
12753        let mut pairs = core::mem::take(&mut head.unions);
12754        let tail = pairs.split_off(boundary);
12755        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12756        for (kind, peer) in tail {
12757            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12758            // An intersect nests into the previous element of THIS
12759            // chain only; with no new previous element it stays at
12760            // the outer level (the left fold applies it to the
12761            // whole head, group included).
12762            match (
12763                is_intersect,
12764                regrouped.len() > boundary,
12765                regrouped.last_mut(),
12766            ) {
12767                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12768                _ => regrouped.push((kind, peer)),
12769            }
12770        }
12771        head.unions = regrouped;
12772        Ok(())
12773    }
12774
12775    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12776    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12777    /// the top-level bare VALUES statement reuses it verbatim.
12778    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12779    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12780    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12781    /// where the grouping-set universe is still in scope.
12782    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12783        if !matches!(self.peek(), Token::Order) {
12784            return Ok(Vec::new());
12785        }
12786        self.advance();
12787        if !self.peek_is_by() {
12788            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12789        }
12790        self.advance();
12791        let mut keys = Vec::new();
12792        loop {
12793            // v7.39 (round 691) — save/restore, the discipline this parser
12794            // already uses around `pending_sample_preds`, so a subquery inside
12795            // a key neither inherits nor leaks the channel.
12796            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12797            let saved_coll = self.order_key_collation.take();
12798            let parsed = self.parse_expr(0);
12799            self.in_order_by_key = saved_flag;
12800            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12801            let expr = parsed?;
12802            let desc = if matches!(self.peek(), Token::Desc) {
12803                self.advance();
12804                true
12805            } else if matches!(self.peek(), Token::Asc) {
12806                self.advance();
12807                false
12808            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12809                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12810                // one ordering per type, so the btree comparison operators map
12811                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12812                // would need a custom operator class — honest error.
12813                self.advance();
12814                match self.advance() {
12815                    Token::Lt | Token::LtEq => false,
12816                    Token::Gt | Token::GtEq => true,
12817                    other => {
12818                        return Err(self.err(alloc::format!(
12819                            "ORDER BY USING supports the btree comparison \
12820                             operators (< <= > >=); got {other:?}"
12821                        )));
12822                    }
12823                }
12824            } else {
12825                false
12826            };
12827            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12828            let nulls_first = self.parse_optional_nulls_placement()?;
12829            keys.push(OrderBy {
12830                expr,
12831                desc,
12832                nulls_first,
12833                collation,
12834            });
12835            if matches!(self.peek(), Token::Comma) {
12836                self.advance();
12837            } else {
12838                break;
12839            }
12840        }
12841        Ok(keys)
12842    }
12843
12844    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12845        // v7.39 (round 135) — a grouping-set query may have already parsed +
12846        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12847        // no ORDER BY token is present, keep that pre-set order_by rather than
12848        // clobbering it with an empty list.
12849        let parsed_keys = self.parse_order_by_keys()?;
12850        head.order_by = if parsed_keys.is_empty() {
12851            core::mem::take(&mut head.order_by)
12852        } else {
12853            parsed_keys
12854        };
12855        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12856        // order. PG's grammar takes a limit clause and an offset clause
12857        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12858        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12859        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12860        // spelling died on `expected end of input, got Limit`.
12861        //
12862        // Each may appear at most once, and LIMIT and FETCH FIRST are
12863        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12864        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12865        // A second one is left unconsumed here, which the caller reports
12866        // as trailing input rather than silently taking the last.
12867        let mut saw_limit = false;
12868        let mut saw_offset = false;
12869        loop {
12870            if !saw_limit && matches!(self.peek(), Token::Limit) {
12871                self.advance();
12872                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12873                // PG synonyms for "no limit". Treat both as None
12874                // (no head.limit set) so the engine's existing
12875                // unlimited-result path takes over. Reject was the
12876                // pre-5.1 behaviour and broke pg_dump-flavoured
12877                // tooling that occasionally emits LIMIT NULL.
12878                if self.consume_limit_unbounded_sentinel() {
12879                    head.limit = None;
12880                } else {
12881                    let first = self.parse_limit_expr("LIMIT")?;
12882                    // MySQL `LIMIT offset, count` — the first number is
12883                    // the offset when a comma follows.
12884                    if matches!(self.peek(), Token::Comma) {
12885                        self.advance();
12886                        let count = self.parse_limit_expr("LIMIT")?;
12887                        head.offset = Some(first);
12888                        saw_offset = true;
12889                        head.limit = Some(count);
12890                    } else {
12891                        head.limit = Some(first);
12892                    }
12893                }
12894                saw_limit = true;
12895                continue;
12896            }
12897            if !saw_offset && matches!(self.peek(), Token::Offset) {
12898                self.advance();
12899                // PG also accepts an optional `ROW` / `ROWS` trailer
12900                // after the offset value (`OFFSET 10 ROWS`). The
12901                // FETCH-FIRST branch below relies on the same.
12902                let off = self.parse_limit_expr("OFFSET")?;
12903                self.consume_optional_rows_keyword();
12904                head.offset = Some(off);
12905                saw_offset = true;
12906                continue;
12907            }
12908            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12909            // the SQL-standard alias for LIMIT. PG accepts both
12910            // spellings interchangeably; pg_dump emits FETCH FIRST in
12911            // newer versions. We map it onto `head.limit` so the
12912            // engine path is unified.
12913            if !saw_limit
12914                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12915                    if s.eq_ignore_ascii_case("fetch"))
12916            {
12917                self.advance(); // FETCH
12918                // `FIRST` or `NEXT` (both legal per SQL standard).
12919                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12920                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12921                {
12922                    self.advance();
12923                }
12924                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12925                // implicit 1 — but we always consume one if present).
12926                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12927                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12928                {
12929                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12930                    crate::ast::LimitExpr::Literal(1)
12931                } else {
12932                    self.parse_limit_expr("FETCH FIRST")?
12933                };
12934                // Eat `ROW` / `ROWS` if not already consumed above.
12935                self.consume_optional_rows_keyword();
12936                // Optional `ONLY` (the spec form) — or the SQL:2008
12937                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12938                // now honours WITH TIES by extending past the LIMIT
12939                // truncation point through every row that shares the
12940                // last-kept row's ORDER BY key.
12941                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12942                    if s.eq_ignore_ascii_case("only"))
12943                {
12944                    self.advance();
12945                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12946                    if s.eq_ignore_ascii_case("with"))
12947                {
12948                    self.advance(); // WITH
12949                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12950                        if s.eq_ignore_ascii_case("ties"))
12951                    {
12952                        self.advance();
12953                        head.limit_with_ties = true;
12954                    }
12955                }
12956                head.limit = Some(count);
12957                saw_limit = true;
12958                continue;
12959            }
12960            break;
12961        }
12962        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12963        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12964        //       [ OF table_name [, …] ]
12965        //       [ NOWAIT | SKIP LOCKED ]
12966        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12967        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12968        // SELECT already returns a consistent snapshot — so these
12969        // are accept-and-discard: the parser absorbs them so
12970        // mailrs / Rails / Django code paths that emit `SELECT
12971        // … FOR UPDATE` for advisory pessimistic locking load
12972        // without a parser error. The on-disk locking model is
12973        // unchanged; callers that rely on FOR UPDATE for read-
12974        // through-write ordering still get the right answer
12975        // because SPG serialises writes anyway.
12976        head.locking = self
12977            .consume_optional_for_lock_clauses()
12978            .map(alloc::boxed::Box::new);
12979        Ok(())
12980    }
12981
12982    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
12983    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
12984    /// LOCKED ]` trailers. Each clause is fully accepted and
12985    /// discarded — SPG's single-writer model already satisfies the
12986    /// callers' implicit ordering requirement. Stops at the first
12987    /// token that isn't `FOR`.
12988    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
12989        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
12990        // not discarded. PG keeps the strongest of several clauses; the
12991        // policy of the last one wins, which is what this loop records.
12992        let mut seen: Option<crate::ast::LockingClause> = None;
12993        while matches!(self.peek(), Token::For) {
12994            // v7.37.14 (A2.5-stub) — record that this query asked
12995            // for a row lock the parser is about to silently
12996            // discard. Operators surface the count via
12997            // `spg_sql::silent_for_update_count()` so they can
12998            // gauge how much of the workload depends on advisory
12999            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
13000            // before v7.37.15's per-row tuple locking lands.
13001            crate::record_silent_for_update_clause();
13002            self.advance(); // FOR
13003            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
13004            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
13005            let mut no_key = false;
13006            let mut key = false;
13007            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13008                if s.eq_ignore_ascii_case("no"))
13009            {
13010                self.advance(); // NO
13011                no_key = true;
13012                // The next ident should be KEY but be generous;
13013                // anything followed by UPDATE/SHARE is accepted.
13014                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13015                    if s.eq_ignore_ascii_case("key"))
13016                {
13017                    self.advance(); // KEY
13018                }
13019            }
13020            // `KEY` prefix (PG `FOR KEY SHARE`).
13021            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13022                if s.eq_ignore_ascii_case("key"))
13023            {
13024                self.advance(); // KEY
13025                key = true;
13026            }
13027            // Lock-strength keyword: UPDATE / SHARE. Required, but
13028            // we're lenient — an unexpected token here just bails
13029            // (we already consumed FOR; caller's downstream
13030            // dispatch will error if anything actually depends on
13031            // the trailing tokens).
13032            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13033                if s.eq_ignore_ascii_case("update"));
13034            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13035                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
13036            {
13037                self.advance();
13038                use crate::ast::LockStrength as LS;
13039                let strength = match (is_update, no_key, key) {
13040                    (true, true, _) => LS::NoKeyUpdate,
13041                    (true, _, _) => LS::Update,
13042                    (false, _, true) => LS::KeyShare,
13043                    (false, _, _) => LS::Share,
13044                };
13045                seen = Some(crate::ast::LockingClause {
13046                    strength,
13047                    of_tables: alloc::vec::Vec::new(),
13048                    policy: crate::ast::LockWait::Wait,
13049                });
13050            } else {
13051                // FOR by itself (or `FOR KEY` with nothing after) —
13052                // give up on the lock-clause path. We've already
13053                // advanced past FOR; further attempts to parse
13054                // here would clobber state.
13055                return seen;
13056            }
13057            // Optional `OF tbl[, tbl …]`. mailrs emits this when
13058            // joining and locking only a subset of tables.
13059            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13060                if s.eq_ignore_ascii_case("of"))
13061            {
13062                self.advance(); // OF
13063                #[allow(clippy::while_let_loop)]
13064                loop {
13065                    match self.peek() {
13066                        Token::Ident(_) | Token::QuotedIdent(_) => {
13067                            // v7.39 (round 294) — the name is CAPTURED now: PG
13068                            // validates it against the FROM clause, and an
13069                            // uncaptured list silently means "lock everything".
13070                            let mut nm = match self.advance() {
13071                                Token::Ident(n) | Token::QuotedIdent(n) => n,
13072                                _ => alloc::string::String::new(),
13073                            };
13074                            // Optional schema-qualified `schema.table`.
13075                            if matches!(self.peek(), Token::Dot) {
13076                                self.advance();
13077                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
13078                                {
13079                                    self.advance();
13080                                    nm = n;
13081                                }
13082                            }
13083                            if let Some(c) = seen.as_mut() {
13084                                c.of_tables.push(nm);
13085                            }
13086                        }
13087                        _ => break,
13088                    }
13089                    if matches!(self.peek(), Token::Comma) {
13090                        self.advance();
13091                    } else {
13092                        break;
13093                    }
13094                }
13095            }
13096            // Optional `NOWAIT` | `SKIP LOCKED`.
13097            match self.peek().clone() {
13098                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
13099                    self.advance();
13100                    if let Some(c) = seen.as_mut() {
13101                        c.policy = crate::ast::LockWait::NoWait;
13102                    }
13103                }
13104                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
13105                    self.advance(); // SKIP
13106                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13107                        if s.eq_ignore_ascii_case("locked"))
13108                    {
13109                        self.advance(); // LOCKED
13110                        if let Some(c) = seen.as_mut() {
13111                            c.policy = crate::ast::LockWait::SkipLocked;
13112                        }
13113                    }
13114                }
13115                _ => {}
13116            }
13117            // Loop: PG allows multiple FOR clauses chained.
13118        }
13119        seen
13120    }
13121
13122    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
13123    /// Bind value gets resolved during prepared-statement Execute;
13124    /// the Pratt expression parser would over-accept here (e.g.
13125    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
13126    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
13127    /// sentinel tokens (PG synonyms for "no limit"). Returns true
13128    /// when one was consumed; caller skips the regular
13129    /// limit-value parse and leaves `head.limit` at None.
13130    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
13131        if matches!(self.peek(), Token::Null) {
13132            self.advance();
13133            return true;
13134        }
13135        if matches!(self.peek(), Token::All) {
13136            self.advance();
13137            return true;
13138        }
13139        false
13140    }
13141
13142    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
13143    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
13144    /// SQL-standard shape. No-op when missing.
13145    fn consume_optional_rows_keyword(&mut self) {
13146        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13147            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
13148        {
13149            self.advance();
13150        }
13151    }
13152
13153    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
13154    ///
13155    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
13156    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
13157    /// constant, which is why that spelling keeps the token path below.
13158    ///
13159    /// Constants are folded here rather than carried into the tree: the
13160    /// 15+ execution paths that read the row count go through
13161    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
13162    /// means "no limit". A clause the engine could not resolve would
13163    /// therefore return the WHOLE table instead of failing. Folding at
13164    /// parse time keeps that impossible; a non-constant clause is still
13165    /// a clean error (recorded residual — closing it wants a resolution
13166    /// pre-pass on the simple-query path, where `substitute_placeholders`
13167    /// does not run).
13168    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13169        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
13170        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
13171        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
13172        // ONLY` both work (its grammar takes a c_expr). Both measured
13173        // against PG 18.4 in round 305.
13174        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
13175            return self.parse_limit_constant(label);
13176        }
13177        // One pass, no rewind: `advance()` takes each token by
13178        // `mem::replace`, so a consumed token reads back as Eof and this
13179        // parser cannot backtrack. Everything — bare literal included —
13180        // is therefore folded from the parsed expression rather than
13181        // re-read from the token stream.
13182        let start = self.pos;
13183        let e = self.parse_expr(0)?;
13184        if let crate::ast::Expr::Placeholder(n) = e {
13185            return Ok(crate::ast::LimitExpr::Placeholder(n));
13186        }
13187        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13188        match fold_limit_constant(&e) {
13189            Some(Ok(v)) if v < 0 => Err(ParseError {
13190                message: alloc::format!("{neg_label} must not be negative"),
13191                token_pos: start,
13192            }),
13193            Some(Ok(v)) => u32::try_from(v)
13194                .map(crate::ast::LimitExpr::Literal)
13195                .map_err(|_| ParseError {
13196                    message: alloc::format!("{label} value too large: {v}"),
13197                    token_pos: start,
13198                }),
13199            Some(Err(message)) => Err(ParseError {
13200                message: message.replace("{L}", neg_label),
13201                token_pos: start,
13202            }),
13203            // v7.39 (round 305, V23) — not foldable at parse time
13204            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
13205            // expression; the engine evaluates it once before dispatch.
13206            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
13207        }
13208    }
13209
13210    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13211        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
13212        // coercion rules, not just an integer token: a NUMERIC rounds half
13213        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13214        // refused with PG's wording ("LIMIT must not be negative", 2201W /
13215        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13216        // content, failing as an input-syntax error on the value. General
13217        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13218        // they need an Expr-carrying LimitExpr variant.
13219        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13220        let err_at = |message: alloc::string::String, pos: usize| ParseError {
13221            message,
13222            token_pos: pos,
13223        };
13224        match self.advance() {
13225            Token::Integer(n) if n >= 0 => u32::try_from(n)
13226                .map(crate::ast::LimitExpr::Literal)
13227                .map_err(|_| ParseError {
13228                    message: alloc::format!("{label} value too large: {n}"),
13229                    token_pos: self.consumed_pos(),
13230                }),
13231            Token::Integer(_) => Err(err_at(
13232                alloc::format!("{neg_label} must not be negative"),
13233                self.pos.saturating_sub(1),
13234            )),
13235            Token::Numeric(t) => {
13236                let pos = self.pos.saturating_sub(1);
13237                let v: f64 = t.parse().map_err(|_| {
13238                    err_at(
13239                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13240                        pos,
13241                    )
13242                })?;
13243                if v < 0.0 {
13244                    return Err(err_at(
13245                        alloc::format!("{neg_label} must not be negative"),
13246                        pos,
13247                    ));
13248                }
13249                // Round half away from zero — PG's numeric→bigint cast.
13250                // (no_std: no f64::round; v is non-negative, so truncating
13251                // v + 0.5 is the same thing.)
13252                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13253                let rounded = (v + 0.5) as u64;
13254                u32::try_from(rounded)
13255                    .map(crate::ast::LimitExpr::Literal)
13256                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13257            }
13258            Token::Minus => {
13259                let pos = self.pos.saturating_sub(1);
13260                match self.peek() {
13261                    Token::Integer(_) | Token::Numeric(_) => {
13262                        self.advance();
13263                        Err(err_at(
13264                            alloc::format!("{neg_label} must not be negative"),
13265                            pos,
13266                        ))
13267                    }
13268                    other => Err(err_at(
13269                        alloc::format!(
13270                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13271                        ),
13272                        pos,
13273                    )),
13274                }
13275            }
13276            Token::String(t) => {
13277                let pos = self.pos.saturating_sub(1);
13278                match t.trim().parse::<i64>() {
13279                    Ok(n) if n < 0 => Err(err_at(
13280                        alloc::format!("{neg_label} must not be negative"),
13281                        pos,
13282                    )),
13283                    Ok(n) => u32::try_from(n)
13284                        .map(crate::ast::LimitExpr::Literal)
13285                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13286                    Err(_) => Err(err_at(
13287                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13288                        pos,
13289                    )),
13290                }
13291            }
13292            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13293            other => Err(ParseError {
13294                message: alloc::format!(
13295                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13296                ),
13297                token_pos: self.consumed_pos(),
13298            }),
13299        }
13300    }
13301
13302    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13303    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13304    /// `unions` empty and `order_by` / `limit` `None`; the top-level
13305    /// `parse_select_stmt` is responsible for filling those in.
13306    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13307    /// call in the expression tree to the per-set integer bitmask
13308    /// (PG semantics: one bit per argument, MSB first; 1 = the key
13309    /// is dropped in this grouping set). Runs during the ROLLUP /
13310    /// CUBE / GROUPING SETS expansion, where the set is known.
13311    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13312    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13313    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13314        if let Expr::FunctionCall { name, .. } = expr
13315            && name.eq_ignore_ascii_case("grouping")
13316        {
13317            if !out.iter().any(|e| e == expr) {
13318                out.push(expr.clone());
13319            }
13320            return;
13321        }
13322        match expr {
13323            Expr::Binary { lhs, rhs, .. } => {
13324                Self::collect_grouping_calls(lhs, out);
13325                Self::collect_grouping_calls(rhs, out);
13326            }
13327            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13328                Self::collect_grouping_calls(expr, out)
13329            }
13330            Expr::FunctionCall { args, .. } => {
13331                for a in args {
13332                    Self::collect_grouping_calls(a, out);
13333                }
13334            }
13335            Expr::Case {
13336                operand,
13337                branches,
13338                else_branch,
13339            } => {
13340                if let Some(o) = operand {
13341                    Self::collect_grouping_calls(o, out);
13342                }
13343                for (c, v) in branches {
13344                    Self::collect_grouping_calls(c, out);
13345                    Self::collect_grouping_calls(v, out);
13346                }
13347                if let Some(x) = else_branch {
13348                    Self::collect_grouping_calls(x, out);
13349                }
13350            }
13351            _ => {}
13352        }
13353    }
13354
13355    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13356    /// `grp_exprs[k]` with a reference to the synthetic ordering column
13357    /// `__grp_ord_k` (injected per grouping-set branch).
13358    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13359        if let Expr::FunctionCall { name, .. } = expr
13360            && name.eq_ignore_ascii_case("grouping")
13361        {
13362            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13363                *expr = Expr::Column(crate::ast::ColumnName {
13364                    qualifier: None,
13365                    name: alloc::format!("__grp_ord_{k}"),
13366                });
13367            }
13368            return;
13369        }
13370        match expr {
13371            Expr::Binary { lhs, rhs, .. } => {
13372                Self::rewrite_grouping_to_col(lhs, grp_exprs);
13373                Self::rewrite_grouping_to_col(rhs, grp_exprs);
13374            }
13375            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13376                Self::rewrite_grouping_to_col(expr, grp_exprs)
13377            }
13378            Expr::FunctionCall { args, .. } => {
13379                for a in args {
13380                    Self::rewrite_grouping_to_col(a, grp_exprs);
13381                }
13382            }
13383            Expr::Case {
13384                operand,
13385                branches,
13386                else_branch,
13387            } => {
13388                if let Some(o) = operand {
13389                    Self::rewrite_grouping_to_col(o, grp_exprs);
13390                }
13391                for (c, v) in branches {
13392                    Self::rewrite_grouping_to_col(c, grp_exprs);
13393                    Self::rewrite_grouping_to_col(v, grp_exprs);
13394                }
13395                if let Some(x) = else_branch {
13396                    Self::rewrite_grouping_to_col(x, grp_exprs);
13397                }
13398            }
13399            _ => {}
13400        }
13401    }
13402
13403    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13404    /// as the list of key sets it contributes. A bare expression is one
13405    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13406    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13407    /// the concatenation of its items' sets, where an item is itself an
13408    /// element, a parenthesized key list, or the empty set `()`. A
13409    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13410    /// move together.
13411    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13412        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13413        // ROLLUP ( … ) / CUBE ( … )
13414        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13415            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13416        {
13417            let is_cube = is_kw(self.peek(), "cube");
13418            self.advance(); // ROLLUP / CUBE
13419            self.advance(); // (
13420            let mut units: Vec<Vec<Expr>> = Vec::new();
13421            loop {
13422                if matches!(self.peek(), Token::LParen) {
13423                    // Composite unit: (a, b) rolls up as one.
13424                    self.advance();
13425                    let mut unit = Vec::new();
13426                    if !matches!(self.peek(), Token::RParen) {
13427                        loop {
13428                            unit.push(self.parse_expr(0)?);
13429                            match self.peek() {
13430                                Token::Comma => {
13431                                    self.advance();
13432                                }
13433                                Token::RParen => break,
13434                                other => {
13435                                    return Err(self.err(format!(
13436                                        "expected ',' or ')' in grouping unit, got {other:?}"
13437                                    )));
13438                                }
13439                            }
13440                        }
13441                    }
13442                    self.advance(); // )
13443                    units.push(unit);
13444                } else {
13445                    units.push(alloc::vec![self.parse_expr(0)?]);
13446                }
13447                match self.peek() {
13448                    Token::Comma => {
13449                        self.advance();
13450                    }
13451                    Token::RParen => break,
13452                    other => {
13453                        return Err(self.err(format!(
13454                            "expected ',' or ')' in grouping list, got {other:?}"
13455                        )));
13456                    }
13457                }
13458            }
13459            self.advance(); // )
13460            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13461                units
13462                    .iter()
13463                    .zip(unit_sel.iter())
13464                    .filter(|(_, keep)| **keep)
13465                    .flat_map(|(u, _)| u.iter().cloned())
13466                    .collect()
13467            };
13468            let n = units.len();
13469            if is_cube {
13470                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13471                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13472                    .collect();
13473                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13474                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13475            }
13476            return Ok((0..=n)
13477                .rev()
13478                .map(|keep| {
13479                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13480                    flatten(&sel)
13481                })
13482                .collect());
13483        }
13484        // GROUPING SETS ( item [, item]* )
13485        if is_kw(self.peek(), "grouping")
13486            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13487        {
13488            self.advance(); // GROUPING
13489            self.advance(); // SETS
13490            if !matches!(self.peek(), Token::LParen) {
13491                return Err(self.err(format!(
13492                    "expected '(' after GROUPING SETS, got {:?}",
13493                    self.peek()
13494                )));
13495            }
13496            self.advance(); // outer (
13497            let mut sets: Vec<Vec<Expr>> = Vec::new();
13498            loop {
13499                if matches!(self.peek(), Token::LParen) {
13500                    // A parenthesized key list (or the empty set).
13501                    self.advance();
13502                    let mut set = Vec::new();
13503                    if !matches!(self.peek(), Token::RParen) {
13504                        loop {
13505                            set.push(self.parse_expr(0)?);
13506                            match self.peek() {
13507                                Token::Comma => {
13508                                    self.advance();
13509                                }
13510                                Token::RParen => break,
13511                                other => {
13512                                    return Err(self.err(format!(
13513                                        "expected ',' or ')' in grouping set, got {other:?}"
13514                                    )));
13515                                }
13516                            }
13517                        }
13518                    }
13519                    self.advance(); // )
13520                    sets.push(set);
13521                } else {
13522                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13523                    // bare expression.
13524                    sets.extend(self.parse_grouping_element()?);
13525                }
13526                match self.peek() {
13527                    Token::Comma => {
13528                        self.advance();
13529                    }
13530                    Token::RParen => break,
13531                    other => {
13532                        return Err(self.err(format!(
13533                            "expected ',' or ')' after a grouping set, got {other:?}"
13534                        )));
13535                    }
13536                }
13537            }
13538            self.advance(); // outer )
13539            return Ok(sets);
13540        }
13541        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13542    }
13543
13544    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13545        // v7.38 (read01) — a reference to a key that is dropped in this grouping
13546        // set evaluates to NULL, at any depth. Previously only a *top-level*
13547        // select item equal to a dropped key was nullified, so a key nested in
13548        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13549        // column and failed to resolve against the set's synthetic schema.
13550        if dropped.iter().any(|d| d == expr) {
13551            *expr = Expr::Literal(Literal::Null);
13552            return;
13553        }
13554        if let Expr::FunctionCall { name, args } = expr
13555            && name.eq_ignore_ascii_case("grouping")
13556        {
13557            let mut mask: i64 = 0;
13558            for a in args.iter() {
13559                mask <<= 1;
13560                if dropped.iter().any(|d| d == a) {
13561                    mask |= 1;
13562                }
13563            }
13564            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13565            // literal: a bare integer in a select item is indistinguishable
13566            // from a positional reference once `ORDER BY 1` substitutes the
13567            // item back in, and the round-232 position check then read the
13568            // mask value as an out-of-range position. The cast changes
13569            // nothing semantically (grouping() is integer).
13570            *expr = Expr::Cast {
13571                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13572                target: crate::ast::CastTarget::Int,
13573            };
13574            return;
13575        }
13576        // Generic recursion over the common expression shapes the
13577        // SELECT list uses; anything without child expressions is
13578        // left alone.
13579        match expr {
13580            // v7.40.0 — an AGGREGATE's argument is NOT nullified.
13581            //
13582            // A grouping column is NULL in the OUTPUT of a set that
13583            // drops it, and an aggregate over it still aggregates the
13584            // real values. Measured, over 0,1,2,3:
13585            //
13586            // ```text
13587            //   SELECT qty, SUM(qty) … GROUP BY ROLLUP(qty)
13588            //     PostgreSQL 18.6   the total row is  NULL | 6
13589            //     MySQL 9.7.2       the total row is  NULL | 6
13590            //     SPG 7.39.13                         NULL | NULL
13591            // ```
13592            //
13593            // The round that made this walk descend "at any depth" was
13594            // right about `COALESCE(g,'TOTAL')` and wrong about
13595            // `SUM(g)`: it turned the aggregate's own input into a NULL
13596            // literal, so the grand total of a rollup keyed on the
13597            // summed column answered nothing. Wrong on BOTH faces.
13598            //
13599            // `grouping(…)` is settled above, before this, so it keeps
13600            // reading the dropped set.
13601            Expr::FunctionCall { name, args } => {
13602                if is_aggregate_function_name(name) {
13603                    return;
13604                }
13605                for a in args {
13606                    Self::substitute_grouping_calls(a, dropped);
13607                }
13608            }
13609            Expr::AggregateOrdered { .. } => {}
13610            Expr::Binary { lhs, rhs, .. } => {
13611                Self::substitute_grouping_calls(lhs, dropped);
13612                Self::substitute_grouping_calls(rhs, dropped);
13613            }
13614            Expr::Unary { expr: inner, .. } => {
13615                Self::substitute_grouping_calls(inner, dropped);
13616            }
13617            Expr::Cast { expr: inner, .. } => {
13618                Self::substitute_grouping_calls(inner, dropped);
13619            }
13620            Expr::Case {
13621                operand,
13622                branches,
13623                else_branch,
13624            } => {
13625                if let Some(op) = operand {
13626                    Self::substitute_grouping_calls(op, dropped);
13627                }
13628                for (w, t) in branches {
13629                    Self::substitute_grouping_calls(w, dropped);
13630                    Self::substitute_grouping_calls(t, dropped);
13631                }
13632                if let Some(e) = else_branch {
13633                    Self::substitute_grouping_calls(e, dropped);
13634                }
13635            }
13636            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13637            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13638            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13639            // …` is the canonical rollup-total label idiom).
13640            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13641            Expr::Like { expr, pattern, .. } => {
13642                Self::substitute_grouping_calls(expr, dropped);
13643                Self::substitute_grouping_calls(pattern, dropped);
13644            }
13645            Expr::InList { expr, list, .. } => {
13646                Self::substitute_grouping_calls(expr, dropped);
13647                for item in list {
13648                    Self::substitute_grouping_calls(item, dropped);
13649                }
13650            }
13651            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13652            Expr::Array(items) => {
13653                for item in items {
13654                    Self::substitute_grouping_calls(item, dropped);
13655                }
13656            }
13657            Expr::ArraySubscript { target, index } => {
13658                Self::substitute_grouping_calls(target, dropped);
13659                Self::substitute_grouping_calls(index, dropped);
13660            }
13661            Expr::ArraySlice { target, lo, hi } => {
13662                Self::substitute_grouping_calls(target, dropped);
13663                if let Some(lo) = lo {
13664                    Self::substitute_grouping_calls(lo, dropped);
13665                }
13666                if let Some(hi) = hi {
13667                    Self::substitute_grouping_calls(hi, dropped);
13668                }
13669            }
13670            Expr::AnyAll { expr, array, .. } => {
13671                Self::substitute_grouping_calls(expr, dropped);
13672                Self::substitute_grouping_calls(array, dropped);
13673            }
13674            _ => {}
13675        }
13676    }
13677
13678    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13679        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13680        // group: `( <select chain> )` usable anywhere a query block
13681        // is (head or peer of an outer chain). The group's own
13682        // unions ride the returned SelectStatement; the executor's
13683        // nested-peer recursion runs them.
13684        if matches!(self.peek(), Token::LParen)
13685            && matches!(
13686                self.tokens.get(self.pos + 1),
13687                Some(Token::Select | Token::LParen | Token::Values)
13688            )
13689        {
13690            self.advance(); // (
13691            self.enter_nested()?;
13692            // v7.37 D.20 — a group whose head is a VALUES list:
13693            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13694            // otherwise recurse into a nested SELECT/group head.
13695            let mut head = (if matches!(self.peek(), Token::Values) {
13696                self.advance(); // VALUES
13697                self.parse_values_rows_body()
13698            } else {
13699                self.parse_bare_select()
13700            })
13701            .and_then(|mut h| {
13702                self.parse_setop_chain_into(&mut h)?;
13703                Ok(h)
13704            });
13705            self.nest_depth -= 1;
13706            let mut head = match &mut head {
13707                Ok(h) => core::mem::take(h),
13708                Err(_) => return head,
13709            };
13710            // v7.37.17 (17.6 siblings) — group-internal tail:
13711            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13712            // group head, then wrap the group as a derived table
13713            // (SELECT * FROM (group)) so the outer chain / outer
13714            // tail can't clobber the group's own ordering or limit.
13715            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13716                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13717                    if s.eq_ignore_ascii_case("fetch"));
13718            if has_tail {
13719                self.parse_select_tail_into(&mut head)?;
13720                head = SelectStatement {
13721                    locking: None,
13722                    ctes: Vec::new(),
13723                    distinct: false,
13724                    distinct_on: Vec::new(),
13725                    items: alloc::vec![SelectItem::Wildcard],
13726                    from: Some(FromClause {
13727                        primary: TableRef {
13728                            name: "subquery".to_string(),
13729                            alias: None,
13730                            only: false,
13731                            as_of_segment: None,
13732                            unnest_expr: None,
13733                            unnest_column_aliases: Vec::new(),
13734                            with_ordinality: false,
13735                            generate_series_args: None,
13736                            lateral_subquery: Some(Box::new(head)),
13737                            jsonb_each_text_arg: None,
13738                            table_fn_call: None,
13739                            rows_from: None,
13740                            json_table: None,
13741                            scalar_fn_item: false,
13742                        },
13743                        joins: Vec::new(),
13744                    }),
13745                    where_: None,
13746                    group_by: None,
13747                    group_by_all: false,
13748                    having: None,
13749                    unions: Vec::new(),
13750                    order_by: Vec::new(),
13751                    limit: None,
13752                    offset: None,
13753                    limit_with_ties: false,
13754                    window_check_exprs: Vec::new(),
13755                };
13756            }
13757            if !matches!(self.peek(), Token::RParen) {
13758                return Err(self.err(format!(
13759                    "expected ')' after parenthesized query group, got {:?}",
13760                    self.peek()
13761                )));
13762            }
13763            self.advance();
13764            return Ok(head);
13765        }
13766        // `TABLE name` shorthand as a query block — valid anywhere
13767        // a SELECT head is (set-op peers included).
13768        if matches!(self.peek(), Token::Table)
13769            && matches!(
13770                self.tokens.get(self.pos + 1),
13771                Some(Token::Ident(_) | Token::QuotedIdent(_))
13772            )
13773        {
13774            return self.parse_table_shorthand();
13775        }
13776        if !matches!(self.peek(), Token::Select) {
13777            return Err(self.err(format!(
13778                "expected SELECT to start a query block, got {:?}",
13779                self.peek()
13780            )));
13781        }
13782        self.advance();
13783        // v7.39.9 — MySQL's `SELECT STRAIGHT_JOIN …` join-order hint.
13784        //
13785        // It sits where `DISTINCT` sits and tells the optimiser to join
13786        // in the written order. SPG plans its own joins, so the hint is
13787        // accepted and not acted on — but it has to PARSE, because as a
13788        // bare identifier it became a column: measured on the published
13789        // image, `SELECT STRAIGHT_JOIN a FROM t` answered `Unknown
13790        // column 'straight_join' in 'field list'` where MySQL 9.7.2
13791        // returns the rows. Only in this position, which is the only one
13792        // MySQL accepts either — a trailing `STRAIGHT_JOIN` is its 1064.
13793        if self.mysql_dialect
13794            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("straight_join"))
13795        {
13796            self.advance();
13797        }
13798        let distinct = if matches!(self.peek(), Token::Distinct) {
13799            self.advance();
13800            true
13801        } else {
13802            false
13803        };
13804        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13805        // keep the first row (per ORDER BY) of each group the
13806        // expressions define. Django's .distinct('field') shape.
13807        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13808            self.advance(); // ON
13809            if !matches!(self.peek(), Token::LParen) {
13810                return Err(self.err(format!(
13811                    "expected '(' after DISTINCT ON, got {:?}",
13812                    self.peek()
13813                )));
13814            }
13815            self.advance();
13816            let mut exprs = Vec::new();
13817            loop {
13818                exprs.push(self.parse_expr(0)?);
13819                match self.peek() {
13820                    Token::Comma => {
13821                        self.advance();
13822                    }
13823                    Token::RParen => break,
13824                    other => {
13825                        return Err(self.err(format!(
13826                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13827                        )));
13828                    }
13829                }
13830            }
13831            self.advance(); // )
13832            exprs
13833        } else {
13834            Vec::new()
13835        };
13836        let mut items = self.parse_select_list()?;
13837        // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13838        // of CTAS. It sits exactly here in PG's grammar, right after the
13839        // target list.
13840        //
13841        // A comment in `ast.rs` has said since v7.38 that CTAS and
13842        // `SELECT INTO` lower to the same node. Only CTAS ever did:
13843        // `SELECT i INTO t FROM src` answered `syntax error at or near
13844        // "INTO"`, which the differential found while measuring what
13845        // PostgreSQL tags each of the five materialising forms with. A
13846        // comment describing a capability the code does not have is the
13847        // defect this version has been finding all day, and this is the
13848        // one it found in the parser.
13849        //
13850        // `INTO` is captured rather than consumed here: the name has to
13851        // travel out of a function that returns a `SelectStatement`, and
13852        // the caller lowers the whole thing to the CTAS node.
13853        if matches!(self.peek(), Token::Into) {
13854            self.advance();
13855            // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13856            // the target, not part of its name. SPG has one storage
13857            // class, so `UNLOGGED` is accepted and means nothing, which
13858            // is what it already means on `CREATE TABLE`.
13859            let mut temporary = false;
13860            loop {
13861                match self.peek().clone() {
13862                    Token::Ident(w) | Token::QuotedIdent(w)
13863                        if w.eq_ignore_ascii_case("temp")
13864                            || w.eq_ignore_ascii_case("temporary") =>
13865                    {
13866                        temporary = true;
13867                        self.advance();
13868                    }
13869                    Token::Ident(w) | Token::QuotedIdent(w)
13870                        if w.eq_ignore_ascii_case("unlogged") =>
13871                    {
13872                        self.advance();
13873                    }
13874                    Token::Table => {
13875                        self.advance();
13876                    }
13877                    _ => break,
13878                }
13879            }
13880            let name = match self.peek().clone() {
13881                Token::Ident(w) | Token::QuotedIdent(w) => {
13882                    self.advance();
13883                    w
13884                }
13885                other => {
13886                    return Err(self.err(alloc::format!(
13887                        "expected a table name after SELECT … INTO, got {other:?}"
13888                    )));
13889                }
13890            };
13891            self.pending_select_into = Some((name, temporary));
13892        }
13893        // Scope the TABLESAMPLE lowering channel to this SELECT:
13894        // stash whatever an enclosing select accumulated, collect
13895        // our own FROM's predicates, restore after the combine.
13896        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13897        let mut from = if matches!(self.peek(), Token::From) {
13898            self.advance();
13899            Some(self.parse_from_clause()?)
13900        } else {
13901            None
13902        };
13903        // v7.37 D.22 — a set-returning function in the projection with no FROM
13904        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13905        // rows. Move the first SRF projection item to a FROM-position derived
13906        // table and replace it in the projection with a reference to its output
13907        // column; sibling scalar columns repeat per SRF row. PG names the output
13908        // column after the function (or its AS alias). Reuses the FROM-SRF
13909        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13910        // works via the targetlist-SRF path.
13911        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13912        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13913        // is exactly what the function's own row shape already is. Anywhere else
13914        // (per outer row, or beside other items) it would need a real record-typed
13915        // projection, so it says so rather than answering something else.
13916        if let [
13917            SelectItem::Expr {
13918                expr: Expr::FunctionCall { name, args },
13919                ..
13920            },
13921        ] = items.as_slice()
13922            && name == "__record_expand"
13923        {
13924            let Some(Expr::FunctionCall {
13925                name: inner_name,
13926                args: inner_args,
13927            }) = args.first()
13928            else {
13929                return Err(self.err(
13930                    "(<expr>).* expands a function's record — it needs a function call".into(),
13931                ));
13932            };
13933            if from.is_some() {
13934                return Err(self.err(
13935                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13936                        .into(),
13937                ));
13938            }
13939            let fn_ref = TableRef {
13940                name: inner_name.clone(),
13941                alias: None,
13942                only: false,
13943                as_of_segment: None,
13944                unnest_expr: None,
13945                unnest_column_aliases: Vec::new(),
13946                with_ordinality: false,
13947                generate_series_args: None,
13948                lateral_subquery: None,
13949                jsonb_each_text_arg: None,
13950                table_fn_call: Some(Box::new((
13951                    inner_name.to_ascii_lowercase(),
13952                    inner_args.clone(),
13953                ))),
13954                rows_from: None,
13955                json_table: None,
13956                scalar_fn_item: false,
13957            };
13958            items = alloc::vec![SelectItem::Wildcard];
13959            from = Some(FromClause {
13960                primary: fn_ref,
13961                joins: Vec::new(),
13962            });
13963        }
13964        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
13965        // FROM, keeps its marker: the ENGINE lowers it, because naming the
13966        // record's fields takes the catalog. It becomes a LATERAL of the same
13967        // function plus one item per declared column — the machinery rounds 65
13968        // and 69 already built.
13969        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
13970        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
13971        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
13972        // express, since the lifted one becomes a scan and the other would
13973        // expand per its rows (a cross product, not a zip). So when the
13974        // projection holds more than one top-level function call, the lift steps
13975        // aside and the engine's target-list expansion takes the whole list.
13976        let fn_call_items = items
13977            .iter()
13978            .filter(|it| {
13979                matches!(
13980                    it,
13981                    SelectItem::Expr {
13982                        expr: Expr::FunctionCall { .. },
13983                        ..
13984                    }
13985                )
13986            })
13987            .count();
13988        if from.is_none() && fn_call_items <= 1 {
13989            let mut found: Option<(usize, TableRef, String)> = None;
13990            for (i, item) in items.iter().enumerate() {
13991                if let SelectItem::Expr {
13992                    expr: Expr::FunctionCall { name, args },
13993                    alias,
13994                } = item
13995                {
13996                    let lname = name.to_ascii_lowercase();
13997                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
13998                    let (unnest, gs) = match lname.as_str() {
13999                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
14000                        "generate_series" if (2..=3).contains(&args.len()) => {
14001                            (None, Some(args.clone()))
14002                        }
14003                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
14004                        // no-FROM projection yields the 1-based subscripts, i.e.
14005                        // generate_series(1, array_length(arr, dim)); an invalid
14006                        // dimension makes array_length NULL → 0 rows, as in PG.
14007                        "generate_subscripts" if args.len() == 2 => (
14008                            None,
14009                            Some(alloc::vec![
14010                                Expr::Literal(Literal::Integer(1)),
14011                                Expr::FunctionCall {
14012                                    name: "array_length".to_string(),
14013                                    args: args.clone(),
14014                                },
14015                            ]),
14016                        ),
14017                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
14018                        // in a no-FROM projection unnest their *_to_array form.
14019                        "string_to_table" | "regexp_split_to_table" => {
14020                            let array_fn = if lname == "string_to_table" {
14021                                "string_to_array"
14022                            } else {
14023                                "regexp_split_to_array"
14024                            };
14025                            (
14026                                Some(Box::new(Expr::FunctionCall {
14027                                    name: array_fn.to_string(),
14028                                    args: args.clone(),
14029                                })),
14030                                None,
14031                            )
14032                        }
14033                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
14034                        // a no-FROM projection expand per element. The scalar form
14035                        // returns the elements as a TEXT array, so unnest over the
14036                        // same call materialises one row each (same rewrite the
14037                        // FROM-clause form uses).
14038                        "jsonb_array_elements"
14039                        | "json_array_elements"
14040                        | "jsonb_array_elements_text"
14041                        | "json_array_elements_text"
14042                            if args.len() == 1 =>
14043                        {
14044                            (
14045                                Some(Box::new(Expr::FunctionCall {
14046                                    name: lname.clone(),
14047                                    args: args.clone(),
14048                                })),
14049                                None,
14050                            )
14051                        }
14052                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
14053                        // in a no-FROM projection expands per match (scalar form
14054                        // returns the matches as a TEXT array → unnest).
14055                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
14056                            Some(Box::new(Expr::FunctionCall {
14057                                name: lname.clone(),
14058                                args: args.clone(),
14059                            })),
14060                            None,
14061                        ),
14062                        _ => continue,
14063                    };
14064                    found = Some((
14065                        i,
14066                        TableRef {
14067                            name: colname.clone(),
14068                            alias: Some(colname.clone()),
14069                            only: false,
14070                            as_of_segment: None,
14071                            unnest_expr: unnest,
14072                            unnest_column_aliases: alloc::vec![colname.clone()],
14073                            with_ordinality: false,
14074                            generate_series_args: gs,
14075                            lateral_subquery: None,
14076                            jsonb_each_text_arg: None,
14077                            table_fn_call: None,
14078                            rows_from: None,
14079                            json_table: None,
14080                            scalar_fn_item: false,
14081                        },
14082                        colname,
14083                    ));
14084                    break;
14085                }
14086            }
14087            if let Some((idx, tref, colname)) = found {
14088                from = Some(FromClause {
14089                    primary: tref,
14090                    joins: Vec::new(),
14091                });
14092                items[idx] = SelectItem::Expr {
14093                    expr: Expr::Column(ColumnName {
14094                        qualifier: None,
14095                        name: colname.clone(),
14096                    }),
14097                    alias: Some(colname),
14098                };
14099            }
14100        }
14101        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
14102        let where_ = if matches!(self.peek(), Token::Where) {
14103            self.advance();
14104            Some(self.parse_expr(0)?)
14105        } else {
14106            None
14107        };
14108        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
14109            Some(match acc {
14110                Some(w) => Expr::Binary {
14111                    lhs: Box::new(pred),
14112                    op: crate::ast::BinOp::And,
14113                    rhs: Box::new(w),
14114                },
14115                None => pred,
14116            })
14117        });
14118        self.pending_sample_preds = enclosing_sample_preds;
14119        let mut group_by_all = false;
14120        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
14121        // share one expansion: `grouping_sets` lists the key subsets
14122        // (first = primary, assigned to stmt.group_by; the rest
14123        // become UNION ALL peers), `grouping_universe` is the full
14124        // key list used to compute each peer's dropped keys.
14125        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
14126        let mut grouping_universe: Vec<Expr> = Vec::new();
14127        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
14128        // A BOOL, not the key list: this frame is the statement parser's, and
14129        // round 430 measured that a `Vec` local here is enough on its own to
14130        // tip the 512 KiB nesting guard. The keys are recoverable from
14131        // `grouping_universe`, which a rollup fills with exactly them.
14132        let mut mysql_rollup = false;
14133        let group_by = if matches!(self.peek(), Token::Group) {
14134            self.advance();
14135            if !self.peek_is_by() {
14136                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
14137            }
14138            self.advance();
14139            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
14140            // every non-aggregate SELECT-list item later.
14141            if matches!(self.peek(), Token::All) {
14142                self.advance();
14143                group_by_all = true;
14144                None
14145            } else {
14146                // v7.39 (round 242) — PG's general grouping-element grammar:
14147                // GROUP BY [DISTINCT] element [, element]*, where an element
14148                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
14149                // SETS (…) — mixed freely. Each element yields a list of
14150                // key sets; the query's grouping sets are the CARTESIAN
14151                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
14152                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
14153                // content. ROLLUP/CUBE members may be composite
14154                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
14155                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
14156                // parser handled only a lone ROLLUP/CUBE/GS as the whole
14157                // clause.
14158                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
14159                    self.advance();
14160                    true
14161                } else {
14162                    false
14163                };
14164                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
14165                loop {
14166                    element_sets.push(self.parse_grouping_element()?);
14167                    if matches!(self.peek(), Token::Comma) {
14168                        self.advance();
14169                    } else {
14170                        break;
14171                    }
14172                }
14173                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
14174                for el in &element_sets {
14175                    let mut next: Vec<Vec<Expr>> = Vec::new();
14176                    for base in &total {
14177                        for set in el {
14178                            let mut merged = base.clone();
14179                            for k in set {
14180                                if !merged.iter().any(|m| m == k) {
14181                                    merged.push(k.clone());
14182                                }
14183                            }
14184                            next.push(merged);
14185                        }
14186                    }
14187                    total = next;
14188                }
14189                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
14190                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
14191                // The keys and the aggregates come out identical; the ROW
14192                // ORDER does not, and that is the part a report depends on.
14193                // MySQL interleaves each group's subtotal right after its
14194                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
14195                // where the union-of-grouping-sets expansion emits every
14196                // leaf first and then every subtotal. MariaDB REFUSES an
14197                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
14198                // order itself — measured on MariaDB 11 and MySQL 9.7, which
14199                // agree on the order and disagree only on whether ORDER BY
14200                // is allowed (MySQL allows it; SPG allows it too, since
14201                // refusing would break the clients that can write it).
14202                if self.mysql_dialect
14203                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
14204                    && matches!(
14205                        self.tokens.get(self.pos + 1),
14206                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
14207                    )
14208                {
14209                    self.advance(); // WITH
14210                    self.advance(); // ROLLUP
14211                    let keys = total.into_iter().next().unwrap_or_default();
14212                    mysql_rollup = true;
14213                    // n+1 prefixes, largest first — the same expansion
14214                    // `ROLLUP (…)` produces.
14215                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
14216                }
14217                if distinct_sets {
14218                    let mut seen: Vec<Vec<String>> = Vec::new();
14219                    total.retain(|set| {
14220                        let mut key: Vec<String> =
14221                            set.iter().map(|e| alloc::format!("{e}")).collect();
14222                        key.sort();
14223                        if seen.contains(&key) {
14224                            false
14225                        } else {
14226                            seen.push(key);
14227                            true
14228                        }
14229                    });
14230                }
14231                if total.len() > 1 {
14232                    let mut universe: Vec<Expr> = Vec::new();
14233                    for set in &total {
14234                        for k in set {
14235                            if !universe.iter().any(|u| u == k) {
14236                                universe.push(k.clone());
14237                            }
14238                        }
14239                    }
14240                    grouping_universe = universe;
14241                    let primary = total[0].clone();
14242                    grouping_sets = total;
14243                    Some(primary)
14244                } else {
14245                    // One set (a plain GROUP BY list, or a single-set
14246                    // spelling like GROUPING SETS ((a, b))). An EMPTY
14247                    // single set — GROUPING SETS (()) — stays
14248                    // `Some(vec![])`: the grand-total group, which must
14249                    // run the aggregate path.
14250                    Some(total.into_iter().next().unwrap_or_default())
14251                }
14252            }
14253        } else {
14254            None
14255        };
14256        let having = if matches!(self.peek(), Token::Having) {
14257            self.advance();
14258            Some(self.parse_expr(0)?)
14259        } else {
14260            None
14261        };
14262        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14263        // OVER w parsed to a marker above; inline each definition
14264        // into the referencing WindowFunction nodes.
14265        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14266        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14267            self.advance();
14268            loop {
14269                let wname = self.expect_ident_like()?;
14270                if !matches!(self.peek(), Token::As) {
14271                    return Err(self.err(format!(
14272                        "expected AS after WINDOW {wname}, got {:?}",
14273                        self.peek()
14274                    )));
14275                }
14276                self.advance();
14277                // v7.39 (round 229) — PG rejects a redefinition outright.
14278                if window_defs
14279                    .iter()
14280                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14281                {
14282                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14283                }
14284                let def = self.parse_over_clause()?;
14285                // A definition may itself copy an earlier one
14286                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14287                // so resolve it against the defs already in scope. Same
14288                // copy rules as an `OVER (w1 …)` in the select list.
14289                let mut probe = Expr::WindowFunction {
14290                    name: String::new(),
14291                    args: Vec::new(),
14292                    partition_by: def.0,
14293                    order_by: def.1,
14294                    frame: def.2,
14295                    null_treatment: crate::ast::NullTreatment::Respect,
14296                    filter: None,
14297                };
14298                Self::substitute_named_windows(&mut probe, &window_defs)
14299                    .map_err(|m| self.err(m))?;
14300                let Expr::WindowFunction {
14301                    partition_by,
14302                    order_by,
14303                    frame,
14304                    ..
14305                } = probe
14306                else {
14307                    unreachable!("probe is a WindowFunction")
14308                };
14309                window_defs.push((wname, (partition_by, order_by, frame)));
14310                if matches!(self.peek(), Token::Comma) {
14311                    self.advance();
14312                    continue;
14313                }
14314                break;
14315            }
14316        }
14317        // v7.39 (round 705) — which definitions did anything reference?
14318        // The ones nothing did used to be dropped here, unexamined, so
14319        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14320        // definition whether referenced or not. Their key expressions ride
14321        // out on the statement for the engine to resolve.
14322        let mut window_refs: Vec<String> = Vec::new();
14323        if !window_defs.is_empty() {
14324            for it in &items {
14325                if let SelectItem::Expr { expr, .. } = it {
14326                    Self::collect_named_window_refs(expr, &mut window_refs);
14327                }
14328            }
14329        }
14330        let window_check_exprs: Vec<Expr> = window_defs
14331            .iter()
14332            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14333            .flat_map(|(_, (partition, order, _))| {
14334                partition
14335                    .iter()
14336                    .cloned()
14337                    .chain(order.iter().map(|(e, _, _)| e.clone()))
14338            })
14339            .collect();
14340        if !window_defs.is_empty()
14341            || items
14342                .iter()
14343                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14344        {
14345            for it in &mut items {
14346                if let SelectItem::Expr { expr, .. } = it {
14347                    Self::substitute_named_windows(expr, &window_defs)
14348                        .map_err(|m| self.err(m))?;
14349                }
14350            }
14351        }
14352        // `GROUP BY 1` — positional keys substitute with the Nth
14353        // select item's expression (same contract ORDER BY has had
14354        // since v6.x). Out-of-range positions error.
14355        let group_by = match group_by {
14356            Some(mut keys) => {
14357                for k in &mut keys {
14358                    if let Expr::Literal(Literal::Integer(n)) = k {
14359                        let idx = *n;
14360                        if idx < 1 || idx as usize > items.len() {
14361                            return Err(self.err(alloc::format!(
14362                                "GROUP BY position {idx} is not in select list"
14363                            )));
14364                        }
14365                        match &items[(idx - 1) as usize] {
14366                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
14367                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14368                                return Err(self.err(alloc::format!(
14369                                    "GROUP BY position {idx} references a wildcard item"
14370                                )));
14371                            }
14372                        }
14373                    }
14374                }
14375                Some(keys)
14376            }
14377            None => None,
14378        };
14379        let mut stmt = SelectStatement {
14380            locking: None,
14381            ctes: Vec::new(),
14382            distinct,
14383            distinct_on,
14384            items,
14385            from,
14386            where_,
14387            group_by,
14388            group_by_all,
14389            having,
14390            unions: Vec::new(),
14391            order_by: Vec::new(),
14392            limit: None,
14393            offset: None,
14394            limit_with_ties: false,
14395            window_check_exprs,
14396        };
14397        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14398        // first set is the primary (already on stmt.group_by); each
14399        // further set becomes a UNION ALL peer with its dropped
14400        // keys (universe minus the set) replaced by NULL literals
14401        // in the peer's items and group_by. PG-legal: non-grouped
14402        // select items must be group keys or aggregates, so a
14403        // dropped key's occurrences in the projection are exactly
14404        // the ones to nullify.
14405        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14406        // over a plain GROUP BY (every argument must be a group key; the
14407        // mask is then 0) and rejects anything else with 42803. SPG's
14408        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14409        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14410        // function `grouping`".
14411        if grouping_sets.len() <= 1 {
14412            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14413            let mut calls: Vec<Expr> = Vec::new();
14414            for item in &stmt.items {
14415                if let SelectItem::Expr { expr, .. } = item {
14416                    Self::collect_grouping_calls(expr, &mut calls);
14417                }
14418            }
14419            if let Some(h) = &stmt.having {
14420                Self::collect_grouping_calls(h, &mut calls);
14421            }
14422            for call in &calls {
14423                let Expr::FunctionCall { args, .. } = call else {
14424                    continue;
14425                };
14426                for a in args {
14427                    if !keys.iter().any(|k| k == a) {
14428                        return Err(self.err(
14429                            "arguments to GROUPING must be grouping expressions of the associated query level"
14430                                .to_string(),
14431                        ));
14432                    }
14433                }
14434            }
14435            if !calls.is_empty() {
14436                for item in &mut stmt.items {
14437                    if let SelectItem::Expr { expr, .. } = item {
14438                        Self::substitute_grouping_calls(expr, &[]);
14439                    }
14440                }
14441                if let Some(h) = &mut stmt.having {
14442                    Self::substitute_grouping_calls(h, &[]);
14443                }
14444            }
14445        }
14446        if grouping_sets.len() > 1 {
14447            // The primary set's own dropped keys nullify in the
14448            // HEAD's projection too (GROUPING SETS's first set may
14449            // omit keys other sets use).
14450            let primary = grouping_sets[0].clone();
14451            let head_dropped: Vec<Expr> = grouping_universe
14452                .iter()
14453                .filter(|u| !primary.iter().any(|k| k == *u))
14454                .cloned()
14455                .collect();
14456            for set in grouping_sets.iter().skip(1) {
14457                let mut peer = stmt.clone();
14458                peer.unions = Vec::new();
14459                let dropped: Vec<&Expr> = grouping_universe
14460                    .iter()
14461                    .filter(|u| !set.iter().any(|k| k == *u))
14462                    .collect();
14463                // Empty set = grand-total group: `Some(vec![])` forces
14464                // the aggregate path (one collapsed row) instead of a
14465                // per-row passthrough. See the primary-set note above.
14466                peer.group_by = Some(set.clone());
14467                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14468                for item in &mut peer.items {
14469                    if let SelectItem::Expr { expr, alias } = item {
14470                        if dropped.iter().any(|d| *d == expr) {
14471                            // v7.39 — keep the dropped key's name on the
14472                            // NULL literal so the UNION output column
14473                            // (and any top-level ORDER BY on it) still
14474                            // resolves.
14475                            if alias.is_none()
14476                                && let Expr::Column(c) = &expr
14477                            {
14478                                *alias = Some(c.name.clone());
14479                            }
14480                            *expr = Expr::Literal(Literal::Null);
14481                        } else {
14482                            Self::substitute_grouping_calls(expr, &dropped_owned);
14483                        }
14484                    }
14485                }
14486                if let Some(h) = &mut peer.having {
14487                    Self::substitute_grouping_calls(h, &dropped_owned);
14488                }
14489                stmt.unions.push((UnionKind::All, peer));
14490            }
14491            for item in &mut stmt.items {
14492                if let SelectItem::Expr { expr, alias } = item {
14493                    if head_dropped.iter().any(|d| d == expr) {
14494                        if alias.is_none()
14495                            && let Expr::Column(c) = &expr
14496                        {
14497                            *alias = Some(c.name.clone());
14498                        }
14499                        *expr = Expr::Literal(Literal::Null);
14500                    } else {
14501                        Self::substitute_grouping_calls(expr, &head_dropped);
14502                    }
14503                }
14504            }
14505            if let Some(h) = &mut stmt.having {
14506                Self::substitute_grouping_calls(h, &head_dropped);
14507            }
14508            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14509            // (while `grouping_universe` / the per-branch sets are in scope). For
14510            // each grouping() call in it, inject a per-branch hidden column
14511            // `__grp_ord_K` carrying that branch's mask into the head + every
14512            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14513            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14514            // from the final output. A standalone grouping-set query has ORDER BY
14515            // (not an explicit set-op) next, so consuming it here is safe.
14516            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14517            // rollup carries the hierarchical order: sort by the grouping
14518            // keys with the rolled-up NULLs last, which is exactly the
14519            // interleaving both oracles emit. A client's own ORDER BY wins,
14520            // which is what MySQL does (MariaDB refuses to let one be
14521            // written at all).
14522            // The synthesised keys have to travel the SAME path a written
14523            // ORDER BY does: the block below is what turns a `grouping()`
14524            // call into the per-branch `__grp_ord_K` column the engine can
14525            // actually sort on. Bypassing it left a bare `grouping(text)`
14526            // for the evaluator to reject.
14527            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14528                self.parse_order_by_keys()?
14529            } else if mysql_rollup {
14530                Self::mysql_rollup_order(&grouping_universe)
14531            } else {
14532                Vec::new()
14533            };
14534            if !synthesised_or_parsed.is_empty() {
14535                let mut order_keys = synthesised_or_parsed;
14536                let mut grp_exprs: Vec<Expr> = Vec::new();
14537                for ob in &order_keys {
14538                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14539                }
14540                for (k, gexpr) in grp_exprs.iter().enumerate() {
14541                    let colname = alloc::format!("__grp_ord_{k}");
14542                    // Head branch (primary set) uses `head_dropped`.
14543                    let mut he = gexpr.clone();
14544                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14545                    stmt.items.push(SelectItem::Expr {
14546                        expr: he,
14547                        alias: Some(colname.clone()),
14548                    });
14549                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14550                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14551                        let set = &grouping_sets[i + 1];
14552                        let dropped: Vec<Expr> = grouping_universe
14553                            .iter()
14554                            .filter(|u| !set.iter().any(|k| k == *u))
14555                            .cloned()
14556                            .collect();
14557                        let mut pe = gexpr.clone();
14558                        Self::substitute_grouping_calls(&mut pe, &dropped);
14559                        peer.items.push(SelectItem::Expr {
14560                            expr: pe,
14561                            alias: Some(colname.clone()),
14562                        });
14563                    }
14564                }
14565                for ob in &mut order_keys {
14566                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14567                }
14568                // v7.40.0 — and a KEY the order sorts on that the query
14569                // did not project.
14570                //
14571                // A UNION's ORDER BY can only name output columns, so
14572                // the rollup order synthesised over `grouping_universe`
14573                // named `qty` for `SELECT SUM(qty) … GROUP BY qty WITH
14574                // ROLLUP` and the query answered `column "qty" does not
14575                // exist`. MySQL 9.7.2 answers 0, 1, 2, 3, 6 — it orders
14576                // by the key whether or not it is selected. The key
14577                // travels as a hidden column, exactly as the grouping
14578                // mask above does, and is stripped from the output by
14579                // the same rule.
14580                let mut key_exprs: Vec<Expr> = Vec::new();
14581                for ob in &order_keys {
14582                    let is_key = grouping_universe.iter().any(|u| u == &ob.expr);
14583                    let projected = stmt
14584                        .items
14585                        .iter()
14586                        .any(|it| matches!(it, SelectItem::Expr { expr, .. } if expr == &ob.expr));
14587                    if is_key && !projected && !key_exprs.iter().any(|k| k == &ob.expr) {
14588                        key_exprs.push(ob.expr.clone());
14589                    }
14590                }
14591                for (k, kexpr) in key_exprs.iter().enumerate() {
14592                    let colname = alloc::format!("__grp_key_{k}");
14593                    let mut he = kexpr.clone();
14594                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14595                    stmt.items.push(SelectItem::Expr {
14596                        expr: he,
14597                        alias: Some(colname.clone()),
14598                    });
14599                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14600                        let set = &grouping_sets[i + 1];
14601                        let dropped: Vec<Expr> = grouping_universe
14602                            .iter()
14603                            .filter(|u| !set.iter().any(|kk| kk == *u))
14604                            .cloned()
14605                            .collect();
14606                        let mut pe = kexpr.clone();
14607                        Self::substitute_grouping_calls(&mut pe, &dropped);
14608                        peer.items.push(SelectItem::Expr {
14609                            expr: pe,
14610                            alias: Some(colname.clone()),
14611                        });
14612                    }
14613                    for ob in &mut order_keys {
14614                        if &ob.expr == kexpr {
14615                            ob.expr = Expr::Column(crate::ast::ColumnName {
14616                                name: colname.clone(),
14617                                qualifier: None,
14618                            });
14619                        }
14620                    }
14621                }
14622                stmt.order_by = order_keys;
14623            }
14624        }
14625        Ok(stmt)
14626    }
14627
14628    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14629    /// as ORDER BY keys.
14630    ///
14631    /// Per key: the rollup marker, then the key. Sorting on the key alone
14632    /// is not enough, and a table with a NULL in it says why — MariaDB puts
14633    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14634    /// the ROLLUP-introduced NULL last, and both print as NULL.
14635    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14636    /// real group including the data-NULL one, 1 only for the row the
14637    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14638    /// rolls up to NULL|2, a|1, b|3, NULL|6.
14639    ///
14640    /// `#[inline(never)]`: its locals must not join the statement parser's
14641    /// frame, which round 430 measured sitting against the nesting guard.
14642    #[inline(never)]
14643    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14644        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14645        for e in keys {
14646            out.push(OrderBy {
14647                expr: Expr::FunctionCall {
14648                    name: "grouping".into(),
14649                    args: alloc::vec![e.clone()],
14650                },
14651                desc: false,
14652                nulls_first: None,
14653                collation: None,
14654            });
14655            out.push(OrderBy {
14656                expr: e.clone(),
14657                desc: false,
14658                // MySQL orders NULL first on an ascending key.
14659                nulls_first: Some(true),
14660                collation: None,
14661            });
14662        }
14663        out
14664    }
14665
14666    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14667    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14668    #[inline(never)]
14669    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14670        use crate::ast::MaintainKind;
14671        self.skip_paren_option_list();
14672        let kind = match self.peek() {
14673            // `TABLE` and `INDEX` lex as keywords, not identifiers.
14674            Token::Table | Token::Index => {
14675                self.advance();
14676                MaintainKind::ReindexRelation
14677            }
14678            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14679                "index" | "table" => {
14680                    self.advance();
14681                    MaintainKind::ReindexRelation
14682                }
14683                "schema" => {
14684                    self.advance();
14685                    MaintainKind::ReindexSchema
14686                }
14687                "system" | "database" => {
14688                    self.advance();
14689                    MaintainKind::Whole
14690                }
14691                // PG requires the object type; anything else is the
14692                // caller's problem, not something to swallow.
14693                _ => MaintainKind::ReindexRelation,
14694            },
14695            _ => MaintainKind::Whole,
14696        };
14697        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14698        // allows the plain form, so the modifier is recorded rather than
14699        // skipped. It still has no effect on how the reindex runs.
14700        let mut concurrently = false;
14701        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14702            self.advance();
14703            concurrently = true;
14704        }
14705        let target = self.take_optional_maintain_name();
14706        self.consume_until_statement_boundary();
14707        Ok(Statement::Maintain {
14708            kind,
14709            concurrently,
14710            target,
14711        })
14712    }
14713
14714    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14715    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14716    #[inline(never)]
14717    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14718        use crate::ast::MaintainKind;
14719        self.skip_paren_option_list();
14720        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14721            self.advance();
14722        }
14723        let target = self.take_optional_maintain_name();
14724        self.consume_until_statement_boundary();
14725        Ok(Statement::Maintain {
14726            kind: if target.is_some() {
14727                MaintainKind::ClusterRelation
14728            } else {
14729                MaintainKind::Whole
14730            },
14731            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14732            // transaction block quite happily (measured).
14733            concurrently: false,
14734            target,
14735        })
14736    }
14737
14738    /// The next token as a relation / schema name, when there is one.
14739    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14740        match self.peek() {
14741            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14742                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14743                _ => None,
14744            },
14745            _ => None,
14746        }
14747    }
14748
14749    /// A parenthesised option list, absorbed.
14750    fn skip_paren_option_list(&mut self) {
14751        if !matches!(self.peek(), Token::LParen) {
14752            return;
14753        }
14754        let mut depth = 0usize;
14755        loop {
14756            match self.advance() {
14757                Token::LParen => depth += 1,
14758                Token::RParen => {
14759                    depth -= 1;
14760                    if depth == 0 {
14761                        return;
14762                    }
14763                }
14764                Token::Eof => return,
14765                _ => {}
14766            }
14767        }
14768    }
14769
14770    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14771    /// column list.
14772    ///
14773    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14774    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14775    /// / ALL. The three that describe physical storage have no meaning
14776    /// here, so they parse and change nothing rather than making a
14777    /// dump that mentions them fail to load.
14778    ///
14779    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14780    /// parse chain the nesting sentinel is tuned against.
14781    #[inline(never)]
14782    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14783        self.advance(); // LIKE
14784        let source = self.expect_ident_like()?;
14785        let mut options = crate::ast::LikeOptions::default();
14786        loop {
14787            let including = match self.peek() {
14788                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14789                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14790                _ => break,
14791            };
14792            self.advance();
14793            // `ALL` lexes as its own keyword, not an identifier.
14794            let opt = if matches!(self.peek(), Token::All) {
14795                self.advance();
14796                alloc::string::String::from("all")
14797            } else {
14798                self.expect_ident_like()?
14799            };
14800            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14801                o.defaults = on;
14802                o.constraints = on;
14803                o.identity = on;
14804                o.generated = on;
14805                o.indexes = on;
14806                o.comments = on;
14807            };
14808            match opt.to_ascii_lowercase().as_str() {
14809                "all" => set(&mut options, including),
14810                "defaults" => options.defaults = including,
14811                "constraints" => options.constraints = including,
14812                "identity" => options.identity = including,
14813                "generated" => options.generated = including,
14814                "indexes" => options.indexes = including,
14815                "comments" => options.comments = including,
14816                // No storage model to copy into.
14817                "storage" | "statistics" | "compression" => {}
14818                other => {
14819                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14820                }
14821            }
14822        }
14823        Ok(crate::ast::LikeSpec {
14824            source,
14825            at,
14826            options,
14827            keep_index_names: false,
14828        })
14829    }
14830
14831    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14832        // Caller already consumed CREATE; we're sitting on TABLE.
14833        debug_assert!(matches!(self.peek(), Token::Table));
14834        self.advance();
14835        let if_not_exists = self.consume_if_not_exists();
14836        let name = self.expect_ident_like()?;
14837        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14838        // child shape has no column list; the child inherits its
14839        // columns from the parent at engine-DDL time. Detect it
14840        // before the `(` requirement below.
14841        if matches!(self.peek(), Token::Partition)
14842            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14843        {
14844            self.advance(); // PARTITION
14845            self.advance(); // of
14846            let partition_of = self.parse_partition_of_tail()?;
14847            return Ok(Statement::CreateTable(CreateTableStatement {
14848                temporary: false,
14849                name,
14850                engine: None,
14851                auto_increment: None,
14852                columns: Vec::new(),
14853                like_specs: Vec::new(),
14854                inherits: Vec::new(),
14855                if_not_exists,
14856                foreign_keys: Vec::new(),
14857                table_constraints: Vec::new(),
14858                partition_by: None,
14859                partition_of: Some(partition_of),
14860            }));
14861        }
14862        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14863        // the materialized-view materialisation path (run the SELECT, infer the
14864        // column types, create + populate the table) but marks the node so the
14865        // executor creates a plain table without a mat-view registry entry.
14866        if matches!(self.peek(), Token::As) {
14867            self.advance();
14868            let body_stmt = self.parse_select_stmt()?;
14869            let Statement::Select(body) = body_stmt else {
14870                return Err(self.err(format!(
14871                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14872                )));
14873            };
14874            let with_data = self.parse_optional_with_data(true)?;
14875            return Ok(Statement::CreateMaterializedView(
14876                crate::ast::CreateMaterializedViewStatement {
14877                    temporary: false,
14878                    name,
14879                    if_not_exists,
14880                    columns: Vec::new(),
14881                    body,
14882                    with_data,
14883                    as_plain_table: true,
14884                },
14885            ));
14886        }
14887        // v7.40.0 — MySQL's `CREATE TABLE b LIKE a`, which is the same
14888        // copy PostgreSQL spells `CREATE TABLE b (LIKE a INCLUDING ALL)`
14889        // written without the parentheses. It was a syntax error, so a
14890        // schema written against MySQL could not be loaded at all.
14891        //
14892        // Measured on MySQL 9.7.2: the copy takes the columns, their
14893        // defaults and the indexes, and takes neither the rows nor the
14894        // foreign keys — which is exactly `INCLUDING ALL` here, since
14895        // SPG's LIKE has never copied foreign keys.
14896        if matches!(self.peek(), Token::Like) {
14897            let at = self.pos;
14898            let spec = self.parse_create_table_like(at)?;
14899            let spec = crate::ast::LikeSpec {
14900                options: crate::ast::LikeOptions {
14901                    defaults: true,
14902                    constraints: true,
14903                    identity: true,
14904                    generated: true,
14905                    indexes: true,
14906                    comments: true,
14907                },
14908                keep_index_names: true,
14909                ..spec
14910            };
14911            return Ok(Statement::CreateTable(CreateTableStatement {
14912                temporary: false,
14913                name,
14914                engine: None,
14915                auto_increment: None,
14916                columns: Vec::new(),
14917                like_specs: alloc::vec![spec],
14918                inherits: Vec::new(),
14919                if_not_exists,
14920                foreign_keys: Vec::new(),
14921                table_constraints: Vec::new(),
14922                partition_by: None,
14923                partition_of: None,
14924            }));
14925        }
14926        if !matches!(self.peek(), Token::LParen) {
14927            return Err(self.err(format!(
14928                "expected '(' after table name, got {:?}",
14929                self.peek()
14930            )));
14931        }
14932        self.advance();
14933        let mut columns = Vec::new();
14934        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14935        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14936        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14937        loop {
14938            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14939            // column list. It is how a child that adds nothing of its own is
14940            // written, and this loop demanded at least one entry: `syntax
14941            // error at or near ")"`. The child takes the parent's columns,
14942            // which the INHERITS clause already arranges.
14943            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14944                self.advance();
14945                break;
14946            }
14947            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14948            // clauses from column definitions. Constraints start
14949            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14950            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14951            // a column.
14952            if self.peek_table_level_pk_start() {
14953                table_constraints.push(self.parse_table_level_primary_key()?);
14954            } else if matches!(self.peek(), Token::Like) {
14955                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
14956                // <opt> ]*`. The source table's shape lives in the catalog,
14957                // so this records the clause and the engine expands it.
14958                like_specs.push(self.parse_create_table_like(columns.len())?);
14959            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
14960                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
14961                table_constraints.push(self.parse_table_level_exclude()?);
14962            } else if self.peek_table_level_unique_start() {
14963                table_constraints.push(self.parse_table_level_unique()?);
14964            } else if self.peek_table_level_check_start() {
14965                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
14966                table_constraints.push(self.parse_table_level_check()?);
14967            } else if self.peek_mysql_inline_key_start() {
14968                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
14969                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
14970                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
14971                // inside the column list. Skip name + paren list;
14972                // for UNIQUE KEY, register as a UC.
14973                if let Some(uc) = self.parse_mysql_inline_key()? {
14974                    table_constraints.push(uc);
14975                }
14976            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
14977                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
14978                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
14979                // CHECK is named, and the named-CONSTRAINT arm used
14980                // to accept FOREIGN KEY only. The name is accepted
14981                // and discarded — same handling as every other SPG
14982                // constraint name.
14983                self.advance(); // CONSTRAINT
14984                // v7.39 (read01 round 48) — the name is kept now: the schema
14985                // stores it, so DROP / RENAME CONSTRAINT can find it.
14986                let con_name = self.expect_ident_like()?;
14987                let mut tc = match kind {
14988                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
14989                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
14990                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
14991                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
14992                };
14993                match &mut tc {
14994                    crate::ast::TableConstraint::Check { name, .. }
14995                    | crate::ast::TableConstraint::Unique { name, .. }
14996                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
14997                    | crate::ast::TableConstraint::Exclude { name, .. } => {
14998                        *name = Some(con_name);
14999                    }
15000                    _ => {}
15001                }
15002                table_constraints.push(tc);
15003            } else if self.peek_constraint_or_fk_start() {
15004                foreign_keys.push(self.parse_table_level_fk()?);
15005            } else {
15006                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
15007                // v7.13.0 — fold inline UNIQUE / CHECK column
15008                // constraints into table-level entries so the
15009                // engine path stays uniform.
15010                if col.is_unique {
15011                    table_constraints.push(crate::ast::TableConstraint::Unique {
15012                        name: None,
15013                        columns: alloc::vec![col.name.clone()],
15014                        nulls_not_distinct: col.unique_nulls_not_distinct,
15015                        deferrable: col.constraint_deferrable,
15016                        initially_deferred: col.constraint_initially_deferred,
15017                        prefix_lengths: Vec::new(),
15018                    });
15019                }
15020                if let Some(check_expr) = col.check.clone() {
15021                    table_constraints.push(crate::ast::TableConstraint::Check {
15022                        name: None,
15023                        expr: check_expr,
15024                        not_valid: false,
15025                    });
15026                }
15027                columns.push(col);
15028                if let Some(fk) = col_level_fk {
15029                    foreign_keys.push(fk);
15030                }
15031            }
15032            match self.peek() {
15033                Token::Comma => {
15034                    self.advance();
15035                }
15036                Token::RParen => {
15037                    self.advance();
15038                    break;
15039                }
15040                other => {
15041                    return Err(
15042                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
15043                    );
15044                }
15045            }
15046        }
15047        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
15048        // `CREATE TABLE k (LIKE t)` is a complete definition even though
15049        // nothing is written between the parentheses.
15050        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
15051        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
15052        // empty parentheses were a parse error in their own right — quite apart
15053        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
15054        // SPG does not have (filed separately).
15055        let _ = &like_specs;
15056        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
15057        // It sits between the column list and the MySQL table options,
15058        // and it was a syntax error until this round.
15059        let mut inherits: Vec<String> = Vec::new();
15060        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
15061            if k.eq_ignore_ascii_case("inherits"))
15062        {
15063            self.advance();
15064            if !matches!(self.peek(), Token::LParen) {
15065                return Err(self.err(alloc::format!(
15066                    "expected ( after INHERITS, got {:?}",
15067                    self.peek()
15068                )));
15069            }
15070            self.advance();
15071            loop {
15072                inherits.push(self.expect_ident_like()?);
15073                if matches!(self.peek(), Token::Comma) {
15074                    self.advance();
15075                    continue;
15076                }
15077                break;
15078            }
15079            if !matches!(self.peek(), Token::RParen) {
15080                return Err(self.err(alloc::format!(
15081                    "expected ) closing INHERITS, got {:?}",
15082                    self.peek()
15083                )));
15084            }
15085            self.advance();
15086        }
15087        // v7.14.0 — consume MySQL/MariaDB table options after the
15088        // closing `)`. mysqldump emits things like
15089        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
15090        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
15091        // SPG accepts all forms as no-ops (each option is
15092        // `<ident> [=] <ident-or-string>` separated by whitespace).
15093        let (engine, auto_increment) = self.consume_mysql_table_options();
15094        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
15095        // SPG has no per-table reloptions, so accept and ignore them so a
15096        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
15097        self.consume_with_reloptions();
15098        // v7.37.6-B — declarative-partition-parent suffix
15099        // (`PARTITION BY RANGE (key_col)`) sits after the column
15100        // list + MySQL table-options. v7.37.6-B only accepts RANGE
15101        // and locks the key column at one ident; the engine then
15102        // verifies the column type is TIMESTAMPTZ.
15103        let partition_by = if matches!(self.peek(), Token::Partition) {
15104            self.advance(); // PARTITION
15105            if !self.peek_is_by() {
15106                return Err(self.err(format!(
15107                    "expected BY after PARTITION, got {:?}",
15108                    self.peek()
15109                )));
15110            }
15111            self.advance();
15112            Some(self.parse_partition_by_tail()?)
15113        } else {
15114            None
15115        };
15116        Ok(Statement::CreateTable(CreateTableStatement {
15117            temporary: false,
15118            name,
15119            engine,
15120            auto_increment,
15121            columns,
15122            like_specs,
15123            inherits,
15124            if_not_exists,
15125            foreign_keys,
15126            table_constraints,
15127            partition_by,
15128            partition_of: None,
15129        }))
15130    }
15131
15132    /// v7.37.6-B — case-insensitive ident match helper for the
15133    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
15134    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
15135    /// didn't burn a global keyword slot for each (see the
15136    /// `Token::Partition` doc-comment in `lexer.rs`).
15137    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
15138        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
15139    }
15140
15141    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
15142    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
15143    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
15144        use crate::ast::{PartitionBySpec, PartitionKindAst};
15145        let kind = match self.peek() {
15146            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
15147                self.advance();
15148                PartitionKindAst::Range
15149            }
15150            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
15151                self.advance();
15152                PartitionKindAst::List
15153            }
15154            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
15155                self.advance();
15156                PartitionKindAst::Hash
15157            }
15158            other => {
15159                return Err(self.err(format!(
15160                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
15161                )));
15162            }
15163        };
15164        if !matches!(self.peek(), Token::LParen) {
15165            return Err(self.err(format!(
15166                "expected '(' after PARTITION BY <strategy>, got {:?}",
15167                self.peek()
15168            )));
15169        }
15170        self.advance();
15171        let mut key_columns = Vec::new();
15172        loop {
15173            key_columns.push(self.expect_ident_like()?);
15174            match self.peek() {
15175                Token::Comma => {
15176                    self.advance();
15177                }
15178                Token::RParen => {
15179                    self.advance();
15180                    break;
15181                }
15182                other => {
15183                    return Err(self.err(format!(
15184                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
15185                    )));
15186                }
15187            }
15188        }
15189        if key_columns.is_empty() {
15190            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
15191        }
15192        Ok(PartitionBySpec { kind, key_columns })
15193    }
15194
15195    /// v7.37.6-B — after `PARTITION OF`, expect
15196    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
15197    /// or
15198    ///   <parent> DEFAULT
15199    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
15200        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
15201        let parent_name = self.expect_ident_like()?;
15202        // v7.37.6-B rejects an explicit column list — the child
15203        // inherits from the parent. mailrs round-7 taught us that
15204        // CREATE TABLE-side schema reconciliation hides drift, so
15205        // we surface this as a parse error rather than silently
15206        // ignoring user columns.
15207        if matches!(self.peek(), Token::LParen) {
15208            return Err(self.err(
15209                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
15210                 at v7.37.6-B; the child inherits its columns from the parent"
15211                    .to_string(),
15212            ));
15213        }
15214        let bounds = match self.peek() {
15215            Token::Default => {
15216                self.advance();
15217                PartitionOfBoundsAst::Default
15218            }
15219            Token::For => {
15220                self.advance();
15221                if !matches!(self.peek(), Token::Values) {
15222                    return Err(
15223                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
15224                    );
15225                }
15226                self.advance();
15227                // WITH is not a reserved Token in the lexer — it lexes
15228                // as Token::Ident("with"). Disambiguate manually.
15229                let want_with = matches!(
15230                    self.peek(),
15231                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15232                );
15233                if want_with {
15234                    self.advance();
15235                    if !matches!(self.peek(), Token::LParen) {
15236                        return Err(self.err(format!(
15237                            "expected '(' after FOR VALUES WITH, got {:?}",
15238                            self.peek()
15239                        )));
15240                    }
15241                    self.advance();
15242                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
15243                    loop {
15244                        let key = self.expect_ident_like()?;
15245                        let n = match self.peek().clone() {
15246                            Token::Integer(v) if u32::try_from(v).is_ok() => {
15247                                self.advance();
15248                                v as u32
15249                            }
15250                            other => {
15251                                return Err(self.err(format!(
15252                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
15253                                )));
15254                            }
15255                        };
15256                        match key.to_ascii_uppercase().as_str() {
15257                            "MODULUS" => modulus = Some(n),
15258                            "REMAINDER" => remainder = Some(n),
15259                            other => {
15260                                return Err(self.err(format!(
15261                                    "FOR VALUES WITH: unknown key {other:?}; \
15262                                     expected MODULUS or REMAINDER"
15263                                )));
15264                            }
15265                        }
15266                        match self.peek() {
15267                            Token::Comma => {
15268                                self.advance();
15269                            }
15270                            Token::RParen => {
15271                                self.advance();
15272                                break;
15273                            }
15274                            other => {
15275                                return Err(self.err(format!(
15276                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
15277                                )));
15278                            }
15279                        }
15280                    }
15281                    let modulus = modulus
15282                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
15283                    let remainder = remainder.ok_or_else(|| {
15284                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
15285                    })?;
15286                    if modulus == 0 {
15287                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
15288                    }
15289                    if remainder >= modulus {
15290                        return Err(self.err(format!(
15291                            "FOR VALUES WITH: REMAINDER ({remainder}) \
15292                             must be < MODULUS ({modulus})"
15293                        )));
15294                    }
15295                    PartitionOfBoundsAst::Hash { modulus, remainder }
15296                } else {
15297                    match self.peek() {
15298                        Token::From => {
15299                            self.advance();
15300                            let lower = Box::new(self.parse_partition_bound_expr()?);
15301                            if !matches!(self.peek(), Token::To) {
15302                                return Err(self.err(format!(
15303                                    "expected TO after FROM (...), got {:?}",
15304                                    self.peek()
15305                                )));
15306                            }
15307                            self.advance();
15308                            let upper = Box::new(self.parse_partition_bound_expr()?);
15309                            PartitionOfBoundsAst::Range { lower, upper }
15310                        }
15311                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
15312                        Token::In => {
15313                            self.advance();
15314                            if !matches!(self.peek(), Token::LParen) {
15315                                return Err(self.err(format!(
15316                                    "expected '(' after FOR VALUES IN, got {:?}",
15317                                    self.peek()
15318                                )));
15319                            }
15320                            self.advance();
15321                            let mut values = Vec::new();
15322                            loop {
15323                                values.push(self.parse_expr(0)?);
15324                                match self.peek() {
15325                                    Token::Comma => {
15326                                        self.advance();
15327                                    }
15328                                    Token::RParen => {
15329                                        self.advance();
15330                                        break;
15331                                    }
15332                                    other => {
15333                                        return Err(self.err(format!(
15334                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
15335                                    )));
15336                                    }
15337                                }
15338                            }
15339                            if values.is_empty() {
15340                                return Err(self.err(
15341                                    "FOR VALUES IN requires at least one literal".to_string(),
15342                                ));
15343                            }
15344                            PartitionOfBoundsAst::List { values }
15345                        }
15346                        other => {
15347                            return Err(self.err(format!(
15348                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
15349                            )));
15350                        }
15351                    }
15352                }
15353            }
15354            other => {
15355                return Err(self.err(format!(
15356                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15357                )));
15358            }
15359        };
15360        Ok(PartitionOfSpec {
15361            parent_name,
15362            bounds,
15363        })
15364    }
15365
15366    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15367    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15368    /// markers (no-arg builtins) so the engine resolves them
15369    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15370    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15371        if !matches!(self.peek(), Token::LParen) {
15372            return Err(self.err(format!(
15373                "expected '(' before partition bound, got {:?}",
15374                self.peek()
15375            )));
15376        }
15377        self.advance();
15378        let expr = match self.peek() {
15379            Token::Ident(s) | Token::QuotedIdent(s)
15380                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15381            {
15382                let name = s.to_ascii_uppercase();
15383                self.advance();
15384                crate::ast::Expr::FunctionCall {
15385                    name,
15386                    args: Vec::new(),
15387                }
15388            }
15389            _ => self.parse_expr(0)?,
15390        };
15391        if !matches!(self.peek(), Token::RParen) {
15392            return Err(self.err(format!(
15393                "expected ')' after partition bound, got {:?}",
15394                self.peek()
15395            )));
15396        }
15397        self.advance();
15398        Ok(expr)
15399    }
15400
15401    /// v7.14.0 — true when the next tokens look like an inline
15402    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15403    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15404    /// — each followed by an optional name + `(...)`. Critical:
15405    /// a column NAMED `key` / `index` (PG accepts as ident) must
15406    /// NOT be mistaken for the KEY constraint shape. We disambig
15407    /// by requiring the keyword to be followed by either `(` or
15408    /// `<ident> (`.
15409    fn peek_mysql_inline_key_start(&self) -> bool {
15410        let cur = self.peek();
15411        // Shapes:
15412        //   KEY (cols)
15413        //   KEY name (cols)
15414        //   INDEX (cols)
15415        //   INDEX name (cols)
15416        //   UNIQUE KEY [name] (cols)
15417        //   UNIQUE INDEX [name] (cols)
15418        //   FULLTEXT [KEY|INDEX] [name] (cols)
15419        //   SPATIAL [KEY|INDEX] [name] (cols)
15420        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15421            // tokens at skip = the position AFTER the index-form
15422            // keywords (KEY/INDEX) have been consumed.
15423            match self.tokens.get(skip) {
15424                Some(Token::LParen) => true,
15425                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15426                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15427                }
15428                _ => false,
15429            }
15430        };
15431        // `INDEX` lexes as Token::Index (reserved), not as
15432        // Token::Ident("index"). Both shapes count as a KEY/INDEX
15433        // start; the peek helper below handles either.
15434        let is_key_or_index_tok = |t: &Token| -> bool {
15435            matches!(t, Token::Index)
15436                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15437        };
15438        match cur {
15439            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15440            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15441                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15442            }
15443            Token::Ident(s)
15444                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15445            {
15446                let nxt = self.tokens.get(self.pos + 1);
15447                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15448                    self.pos + 2
15449                } else {
15450                    self.pos + 1
15451                };
15452                after_keyword_followed_by_paren_or_ident_paren(after_after)
15453            }
15454            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15455                let nxt = self.tokens.get(self.pos + 1);
15456                if !nxt.is_some_and(is_key_or_index_tok) {
15457                    return false;
15458                }
15459                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15460            }
15461            _ => false,
15462        }
15463    }
15464
15465    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15466    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15467    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15468    /// returns Some(TableConstraint::Index) so the engine builds
15469    /// a real BTree index on the leading column (mysqldump
15470    /// `KEY idx_posts_author (author_id)` shape).
15471    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15472    /// (the storage layer has no matching AM).
15473    fn parse_mysql_inline_key(
15474        &mut self,
15475    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15476        // Detect UNIQUE prefix.
15477        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15478        {
15479            self.advance();
15480            true
15481        } else {
15482            false
15483        };
15484        // Consume FULLTEXT / SPATIAL prefix and record which one
15485        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15486        // dedicated TableConstraint variant so the engine can
15487        // build a tsvector-GIN; SPATIAL still has no matching
15488        // AM, so it falls back to accept-as-no-op.
15489        let mut is_fulltext = false;
15490        let mut is_spatial = false;
15491        if let Token::Ident(s) = self.peek().clone() {
15492            if s.eq_ignore_ascii_case("fulltext") {
15493                self.advance();
15494                is_fulltext = true;
15495            } else if s.eq_ignore_ascii_case("spatial") {
15496                self.advance();
15497                is_spatial = true;
15498            }
15499        }
15500        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15501        // (reserved); accept either token shape.
15502        match self.peek() {
15503            Token::Index => {
15504                self.advance();
15505            }
15506            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15507                self.advance();
15508            }
15509            other => {
15510                return Err(self.err(alloc::format!(
15511                    "expected KEY/INDEX in inline index declaration, got {other:?}"
15512                )));
15513            }
15514        }
15515        // Optional index name (an ident before the `(`).
15516        // v7.15.0 — capture the name when present so the engine
15517        // builds the secondary index under the user's chosen
15518        // name (matches mysqldump's `KEY idx_x (col)` shape).
15519        let mut idx_name: Option<String> = None;
15520        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15521            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15522        {
15523            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15524                idx_name = Some(s);
15525            }
15526        }
15527        // Optional `USING BTREE` / `USING HASH` (MySQL).
15528        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15529            self.advance();
15530            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15531                self.advance();
15532            }
15533        }
15534        // Required column list `(col [, col]*)`.
15535        if !matches!(self.peek(), Token::LParen) {
15536            return Err(self.err(alloc::format!(
15537                "expected '(' in inline KEY/INDEX, got {:?}",
15538                self.peek()
15539            )));
15540        }
15541        self.advance();
15542        let mut cols: Vec<String> = Vec::new();
15543        let mut prefix_lengths: Vec<Option<u32>> = Vec::new();
15544        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15545            self.advance();
15546            cols.push(s);
15547            // v7.40.0 — the per-column `(length)` prefix is KEPT.
15548            //
15549            // It used to be skipped, so `KEY kb (b(4))` was accepted and
15550            // the prefix forgotten: `SHOW INDEX` reported `Sub_part`
15551            // NULL and `SHOW CREATE TABLE` printed `(b)` where MySQL
15552            // 9.7.2 prints `(b(4))`. A declaration that is accepted and
15553            // then unrecorded is the worst of the three answers.
15554            let mut prefix: Option<u32> = None;
15555            if matches!(self.peek(), Token::LParen) {
15556                let mut depth = 1usize;
15557                self.advance();
15558                if let Token::Integer(n) = self.peek()
15559                    && let Ok(v) = u32::try_from(*n)
15560                {
15561                    prefix = Some(v);
15562                }
15563                while depth > 0 {
15564                    match self.peek() {
15565                        Token::LParen => depth += 1,
15566                        Token::RParen => depth -= 1,
15567                        Token::Eof => break,
15568                        _ => {}
15569                    }
15570                    self.advance();
15571                }
15572            }
15573            prefix_lengths.push(prefix);
15574            // Skip optional ASC / DESC.
15575            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15576                || matches!(self.peek(), Token::Asc | Token::Desc)
15577            {
15578                self.advance();
15579            }
15580            if matches!(self.peek(), Token::Comma) {
15581                self.advance();
15582                continue;
15583            }
15584            break;
15585        }
15586        if matches!(self.peek(), Token::RParen) {
15587            self.advance();
15588        }
15589        // Trailing options on the inline index — comment / etc.
15590        // Skip until comma or `)`.
15591        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15592            self.advance();
15593        }
15594        if cols.is_empty() {
15595            return Ok(None);
15596        }
15597        if is_unique {
15598            // Carry the captured idx_name on UNIQUE too so future
15599            // engine work can name the underlying BTree
15600            // accordingly; today the unique-constraint installer
15601            // synthesises the name itself, but Display round-trip
15602            // benefits from preserving it.
15603            Ok(Some(crate::ast::TableConstraint::Unique {
15604                name: idx_name,
15605                columns: cols,
15606                nulls_not_distinct: false,
15607                // MySQL inline UNIQUE KEY has no deferral vocabulary.
15608                deferrable: false,
15609                initially_deferred: false,
15610                prefix_lengths,
15611            }))
15612        } else if is_fulltext {
15613            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15614            // routes through `TableConstraint::FulltextIndex`;
15615            // the engine builds a tsvector-GIN over each named
15616            // column so MATCH AGAINST gets a real inverted
15617            // index instead of a silently-dropped declaration.
15618            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15619                name: idx_name,
15620                columns: cols,
15621            }))
15622        } else if is_spatial {
15623            // SPG has no native SPATIAL AM. Accept-as-no-op
15624            // (declaration is parsed, but no index is built).
15625            Ok(None)
15626        } else {
15627            // v7.15.0 — plain KEY / INDEX builds a real BTree
15628            // secondary index.
15629            Ok(Some(crate::ast::TableConstraint::Index {
15630                name: idx_name,
15631                columns: cols,
15632                prefix_lengths,
15633            }))
15634        }
15635    }
15636
15637    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15638    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15639    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15640    /// (in any order, separated by whitespace).
15641    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15642    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15643    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15644    /// bare ident here, and only the parenthesised form is reloptions (so this
15645    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15646    fn consume_with_reloptions(&mut self) {
15647        let is_with = matches!(
15648            self.peek(),
15649            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15650        );
15651        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15652            return;
15653        }
15654        self.advance(); // WITH
15655        self.advance(); // (
15656        let mut depth = 1u32;
15657        while depth > 0 && !matches!(self.peek(), Token::Eof) {
15658            match self.peek() {
15659                Token::LParen => depth += 1,
15660                Token::RParen => depth -= 1,
15661                _ => {}
15662            }
15663            self.advance();
15664        }
15665    }
15666
15667    /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15668    /// dropped with everything else here. The rest of the MySQL table
15669    /// options genuinely have no meaning for SPG's storage; the engine
15670    /// name does, because MySQL REFUSES one it does not know and a dump
15671    /// with a typo in it should not quietly become a table.
15672    fn consume_mysql_table_options(&mut self) -> (Option<alloc::string::String>, Option<i64>) {
15673        let mut engine: Option<alloc::string::String> = None;
15674        let mut auto_increment: Option<i64> = None;
15675        loop {
15676            // Heuristic: a table option is an ident (or `DEFAULT`
15677            // reserved keyword) followed by `=` and an
15678            // ident / string / integer.
15679            let name_lc = match self.peek().clone() {
15680                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15681                Token::Default => alloc::string::String::from("default"),
15682                _ => break,
15683            };
15684            let known = matches!(
15685                name_lc.as_str(),
15686                "engine"
15687                    | "default"
15688                    | "charset"
15689                    | "collate"
15690                    | "auto_increment"
15691                    | "row_format"
15692                    | "comment"
15693                    | "pack_keys"
15694                    | "stats_persistent"
15695                    | "stats_auto_recalc"
15696                    | "stats_sample_pages"
15697                    | "key_block_size"
15698                    | "tablespace"
15699                    | "min_rows"
15700                    | "max_rows"
15701                    | "checksum"
15702                    | "delay_key_write"
15703                    | "insert_method"
15704                    | "data"
15705                    | "index"
15706                    | "encryption"
15707                    | "compression"
15708            );
15709            if !known {
15710                break;
15711            }
15712            self.advance(); // option name
15713            // `DEFAULT` optional prefix is followed by `CHARSET` /
15714            // `COLLATE`; consume the next ident too.
15715            if name_lc == "default" {
15716                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15717                    self.advance();
15718                }
15719            }
15720            if matches!(self.peek(), Token::Eq) {
15721                self.advance();
15722            }
15723            match self.peek().clone() {
15724                Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15725                    if name_lc == "engine" {
15726                        // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15727                        // engine it does not know and names it back
15728                        // exactly: `Unknown storage engine 'NoSuchEng'`,
15729                        // measured. The lexer folds a bare identifier, so
15730                        // the message quoted a name the dump did not
15731                        // contain, which is the one thing that message is
15732                        // for. Guarded the same way the column spelling
15733                        // is: the span runs to the next token, so what
15734                        // comes back has to be the same word.
15735                        let written = self
15736                            .source_span(self.pos, self.pos)
15737                            .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15738                            .filter(|raw| raw.eq_ignore_ascii_case(&v))
15739                            .map(alloc::string::String::from);
15740                        engine = Some(written.unwrap_or(v));
15741                    }
15742                    self.advance();
15743                }
15744                Token::Integer(v) => {
15745                    // v7.40.0 — `AUTO_INCREMENT=100` is the next value
15746                    // the table hands out, and it was consumed and
15747                    // dropped: measured on MySQL 9.7.2, the first row
15748                    // inserted into a table declared that way gets 100,
15749                    // where SPG gave it 1. `SHOW CREATE TABLE` already
15750                    // reproduces the option from the counter, so the
15751                    // dump round-tripped through a different number.
15752                    if name_lc == "auto_increment" {
15753                        auto_increment = Some(v);
15754                    }
15755                    self.advance();
15756                }
15757                _ => {}
15758            }
15759        }
15760        (engine, auto_increment)
15761    }
15762
15763    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15764    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15765    /// sure (otherwise a column literally named `primary` would
15766    /// be mistaken).
15767    fn peek_table_level_pk_start(&self) -> bool {
15768        let cur = self.peek();
15769        let nxt = self.tokens.get(self.pos + 1);
15770        let nxt2 = self.tokens.get(self.pos + 2);
15771        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15772        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15773        let is_lparen = matches!(nxt2, Some(Token::LParen));
15774        is_primary && is_key && is_lparen
15775    }
15776
15777    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15778    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15779    /// (mailrs round-5 G10).
15780    fn peek_table_level_unique_start(&self) -> bool {
15781        let cur = self.peek();
15782        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15783        if !is_unique {
15784            return false;
15785        }
15786        let n1 = self.tokens.get(self.pos + 1);
15787        // Plain `UNIQUE (…)`.
15788        if matches!(n1, Some(Token::LParen)) {
15789            return true;
15790        }
15791        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15792        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15793        if !is_nulls {
15794            return false;
15795        }
15796        let n2 = self.tokens.get(self.pos + 2);
15797        let n3 = self.tokens.get(self.pos + 3);
15798        let n4 = self.tokens.get(self.pos + 4);
15799        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15800        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15801            return true;
15802        }
15803        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15804        if matches!(n2, Some(Token::Not))
15805            && matches!(n3, Some(Token::Distinct))
15806            && matches!(n4, Some(Token::LParen))
15807        {
15808            return true;
15809        }
15810        false
15811    }
15812
15813    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15814        self.advance(); // PRIMARY
15815        self.advance(); // KEY
15816        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15817        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15818        // 621 consumed and dropped them (the storing half of F08).
15819        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15820        Ok(crate::ast::TableConstraint::PrimaryKey {
15821            name: None,
15822            columns,
15823            deferrable,
15824            initially_deferred,
15825        })
15826    }
15827
15828    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15829        self.advance(); // UNIQUE
15830        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15831        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15832        // is `NULLS DISTINCT` per the SQL standard.
15833        let mut nulls_not_distinct = false;
15834        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15835            let n1 = self.tokens.get(self.pos + 1);
15836            let n2 = self.tokens.get(self.pos + 2);
15837            let is_not = matches!(n1, Some(Token::Not));
15838            let is_distinct = matches!(n2, Some(Token::Distinct));
15839            if is_not && is_distinct {
15840                self.advance(); // NULLS
15841                self.advance(); // NOT
15842                self.advance(); // DISTINCT
15843                nulls_not_distinct = true;
15844            } else if matches!(n1, Some(Token::Distinct)) {
15845                self.advance(); // NULLS
15846                self.advance(); // DISTINCT
15847            }
15848        }
15849        let columns = self.parse_paren_ident_list("UNIQUE")?;
15850        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15851        Ok(crate::ast::TableConstraint::Unique {
15852            name: None,
15853            columns,
15854            nulls_not_distinct,
15855            deferrable,
15856            initially_deferred,
15857            prefix_lengths: Vec::new(),
15858        })
15859    }
15860
15861    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15862    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15863    /// expression.
15864    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15865    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15866    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15867    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15868    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15869    /// commit: `NOT` starts no other suffix here, but reading both
15870    /// tokens before advancing keeps the caller's error message intact
15871    /// if someone writes `NOT NULL` by mistake.
15872    fn parse_not_valid_suffix(&mut self) -> bool {
15873        if !matches!(self.peek(), Token::Not) {
15874            return false;
15875        }
15876        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15877        {
15878            return false;
15879        }
15880        self.advance();
15881        self.advance();
15882        true
15883    }
15884
15885    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15886        self.advance(); // EXCLUDE
15887        // Optional `USING <method>`.
15888        let mut method = None;
15889        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15890            self.advance();
15891            method = Some(match self.advance() {
15892                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15893                other => {
15894                    return Err(self.err(alloc::format!(
15895                        "expected index method after USING, got {other:?}"
15896                    )));
15897                }
15898            });
15899        }
15900        if !matches!(self.peek(), Token::LParen) {
15901            return Err(self.err(alloc::format!(
15902                "expected '(' after EXCLUDE, got {:?}",
15903                self.peek()
15904            )));
15905        }
15906        self.advance();
15907        let mut elements: Vec<(String, String)> = Vec::new();
15908        loop {
15909            let col = match self.advance() {
15910                Token::Ident(s) | Token::QuotedIdent(s) => s,
15911                other => {
15912                    return Err(self.err(alloc::format!(
15913                        "expected column name in EXCLUDE, got {other:?}"
15914                    )));
15915                }
15916            };
15917            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15918                return Err(self.err(alloc::format!(
15919                    "expected WITH after EXCLUDE column, got {:?}",
15920                    self.peek()
15921                )));
15922            }
15923            self.advance();
15924            let op = match self.advance() {
15925                Token::InetOverlap => String::from("&&"),
15926                Token::Intersects => String::from("?#"),
15927                Token::IsBelow => String::from("<^"),
15928                Token::IsAbove => String::from(">^"),
15929                Token::PatternLt => String::from("~<~"),
15930                Token::PatternLtEq => String::from("~<=~"),
15931                Token::PatternGt => String::from("~>~"),
15932                Token::PatternGtEq => String::from("~>=~"),
15933                Token::TsMatchOld => String::from("@@@"),
15934                Token::Eq => String::from("="),
15935                Token::JsonContains => String::from("@>"),
15936                Token::JsonContainedBy => String::from("<@"),
15937                Token::OverLeft => String::from("&<"),
15938                Token::OverRight => String::from("&>"),
15939                other => {
15940                    return Err(self.err(alloc::format!(
15941                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15942                    )));
15943                }
15944            };
15945            elements.push((col, op));
15946            if matches!(self.peek(), Token::Comma) {
15947                self.advance();
15948                continue;
15949            }
15950            break;
15951        }
15952        if !matches!(self.peek(), Token::RParen) {
15953            return Err(self.err(alloc::format!(
15954                "expected ')' to close EXCLUDE, got {:?}",
15955                self.peek()
15956            )));
15957        }
15958        self.advance();
15959        Ok(crate::ast::TableConstraint::Exclude {
15960            name: None,
15961            method,
15962            elements,
15963        })
15964    }
15965
15966    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15967        self.advance(); // CHECK
15968        if !matches!(self.peek(), Token::LParen) {
15969            return Err(self.err(alloc::format!(
15970                "expected '(' after CHECK, got {:?}",
15971                self.peek()
15972            )));
15973        }
15974        self.advance();
15975        let expr = self.parse_expr(0)?;
15976        if !matches!(self.peek(), Token::RParen) {
15977            return Err(self.err(alloc::format!(
15978                "expected ')' to close CHECK predicate, got {:?}",
15979                self.peek()
15980            )));
15981        }
15982        self.advance();
15983        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
15984        // are no existing rows for PG to skip, so it rejects the suffix.
15985        Ok(crate::ast::TableConstraint::Check {
15986            name: None,
15987            expr,
15988            not_valid: false,
15989        })
15990    }
15991
15992    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
15993    fn peek_table_level_check_start(&self) -> bool {
15994        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
15995    }
15996
15997    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
15998    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
15999    /// on the dedicated FK path (`parse_table_level_fk` consumes its
16000    /// own CONSTRAINT prefix).
16001    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
16002        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16003            return None;
16004        }
16005        // tokens[pos+1] is the constraint name (any ident-like);
16006        // tokens[pos+2] is the kind keyword.
16007        match self.tokens.get(self.pos + 2) {
16008            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
16009                Some(NamedTableConstraintKind::Check)
16010            }
16011            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
16012                Some(NamedTableConstraintKind::Unique)
16013            }
16014            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
16015                Some(NamedTableConstraintKind::PrimaryKey)
16016            }
16017            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
16018                Some(NamedTableConstraintKind::Exclude)
16019            }
16020            _ => None,
16021        }
16022    }
16023
16024    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
16025        if !matches!(self.peek(), Token::LParen) {
16026            return Err(self.err(alloc::format!(
16027                "expected '(' after {ctx}, got {:?}",
16028                self.peek()
16029            )));
16030        }
16031        self.advance();
16032        let mut out = Vec::new();
16033        loop {
16034            out.push(self.expect_ident_like()?);
16035            match self.peek() {
16036                Token::Comma => {
16037                    self.advance();
16038                }
16039                Token::RParen => {
16040                    self.advance();
16041                    break;
16042                }
16043                other => {
16044                    return Err(self.err(alloc::format!(
16045                        "expected ',' or ')' in {ctx} list, got {other:?}"
16046                    )));
16047                }
16048            }
16049        }
16050        if out.is_empty() {
16051            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
16052        }
16053        Ok(out)
16054    }
16055
16056    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
16057    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
16058    /// table-level FK; a column def never starts with either keyword
16059    /// (column names are not in this reserved set).
16060    fn peek_constraint_or_fk_start(&self) -> bool {
16061        let is_constraint_kw = matches!(
16062            self.peek(),
16063            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
16064        );
16065        let is_foreign_kw = matches!(
16066            self.peek(),
16067            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
16068        );
16069        is_constraint_kw || is_foreign_kw
16070    }
16071
16072    /// v7.6.0 — parse a table-level FK clause:
16073    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
16074    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
16075    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
16076        let mut name: Option<String> = None;
16077        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16078            self.advance();
16079            name = Some(self.expect_ident_like()?);
16080        }
16081        // `FOREIGN`
16082        match self.advance() {
16083            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
16084            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
16085        }
16086        // `KEY`
16087        match self.advance() {
16088            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
16089            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
16090        }
16091        // `(col, col, ...)`
16092        if !matches!(self.peek(), Token::LParen) {
16093            return Err(self.err(format!(
16094                "expected '(' after FOREIGN KEY, got {:?}",
16095                self.peek()
16096            )));
16097        }
16098        self.advance();
16099        let mut columns = Vec::new();
16100        loop {
16101            columns.push(self.expect_ident_like()?);
16102            match self.peek() {
16103                Token::Comma => {
16104                    self.advance();
16105                }
16106                Token::RParen => {
16107                    self.advance();
16108                    break;
16109                }
16110                other => {
16111                    return Err(self.err(format!(
16112                        "expected ',' or ')' in FK column list, got {other:?}"
16113                    )));
16114                }
16115            }
16116        }
16117        if columns.is_empty() {
16118            return Err(self.err("FOREIGN KEY requires at least one column".into()));
16119        }
16120        let (
16121            parent_table,
16122            parent_columns,
16123            on_delete,
16124            on_update,
16125            match_type,
16126            deferrable,
16127            initially_deferred,
16128        ) = self.parse_references_tail(columns.len())?;
16129        Ok(ForeignKeyConstraint {
16130            name,
16131            columns,
16132            parent_table,
16133            parent_columns,
16134            on_delete,
16135            on_update,
16136            match_type,
16137            deferrable,
16138            initially_deferred,
16139        })
16140    }
16141
16142    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
16143    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
16144    /// the local column count, used to default the parent column
16145    /// list when omitted (SQL spec: parent's PK is implied).
16146    fn parse_references_tail(
16147        &mut self,
16148        expected_arity: usize,
16149    ) -> Result<
16150        (
16151            String,
16152            Vec<String>,
16153            FkAction,
16154            FkAction,
16155            crate::ast::MatchType,
16156            // v7.39 (round 288) — deferrable, initially_deferred.
16157            bool,
16158            bool,
16159        ),
16160        ParseError,
16161    > {
16162        match self.advance() {
16163            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
16164            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
16165        }
16166        let parent_table = self.expect_ident_like()?;
16167        let mut parent_columns: Vec<String> = Vec::new();
16168        if matches!(self.peek(), Token::LParen) {
16169            self.advance();
16170            loop {
16171                parent_columns.push(self.expect_ident_like()?);
16172                match self.peek() {
16173                    Token::Comma => {
16174                        self.advance();
16175                    }
16176                    Token::RParen => {
16177                        self.advance();
16178                        break;
16179                    }
16180                    other => {
16181                        return Err(self.err(format!(
16182                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
16183                        )));
16184                    }
16185                }
16186            }
16187        }
16188        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
16189            return Err(self.err(format!(
16190                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
16191                expected_arity,
16192                parent_columns.len()
16193            )));
16194        }
16195        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
16196        // it between the referenced column list and the ON / DEFERRABLE
16197        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
16198        // is skipped when any referencing column is NULL), so SIMPLE —
16199        // the default, and the only spelling pg_dump emits — is accepted
16200        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
16201        // mixed-NULL rule, which is not wired yet; reject them honestly
16202        // rather than silently applying SIMPLE (PG itself errors on
16203        // MATCH PARTIAL as "not yet implemented").
16204        let mut match_type = crate::ast::MatchType::Simple;
16205        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
16206            self.advance();
16207            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
16208            // SIMPLE / PARTIAL arrive as bare identifiers.
16209            let kind = match self.advance() {
16210                Token::Full => "FULL".to_string(),
16211                Token::Ident(s) => s.to_uppercase(),
16212                other => {
16213                    return Err(self.err(format!(
16214                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
16215                    )));
16216                }
16217            };
16218            match kind.as_str() {
16219                "SIMPLE" => {} // Default — match_type stays Simple.
16220                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
16221                // when ALL referencing columns are NULL; a mixed-NULL key errors.
16222                "FULL" => match_type = crate::ast::MatchType::Full,
16223                "PARTIAL" => {
16224                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
16225                }
16226                _ => {
16227                    return Err(self.err(format!(
16228                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
16229                    )));
16230                }
16231            }
16232        }
16233        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
16234        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
16235        // <action>` / `ON UPDATE <action>` in either order. PG /
16236        // pg_dump emits the timing clause AFTER the ON clauses
16237        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
16238        // but the SQL spec allows either order. We loop over
16239        // every possible trailer and dispatch on the next token,
16240        // stopping when nothing matches. Phase 3.1 changes the
16241        // bare DEFERRABLE form from hard-error to accept-as-
16242        // immediate; SPG is single-writer with no deferred-
16243        // constraint window so the runtime semantics are always
16244        // immediate even when INITIALLY DEFERRED is requested.
16245        // PG's default referential action (no ON DELETE / ON UPDATE
16246        // clause) is NO ACTION, not RESTRICT — the two enforce
16247        // identically in SPG (single-writer, no deferred window; see the
16248        // shared match arm in constraints.rs) but information_schema.
16249        // referential_constraints must report NO ACTION to match PG.
16250        let mut on_delete = FkAction::NoAction;
16251        let mut on_update = FkAction::NoAction;
16252        let mut seen_on_delete = false;
16253        let mut seen_on_update = false;
16254        let mut deferrable = false;
16255        let mut initially_deferred = false;
16256        loop {
16257            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
16258            let before = self.pos;
16259            let (d, idef) = self.consume_deferrable_clauses_timed()?;
16260            if self.pos != before {
16261                deferrable = d;
16262                initially_deferred = idef;
16263                continue;
16264            }
16265            // ON DELETE / ON UPDATE.
16266            if !matches!(self.peek(), Token::On) {
16267                break;
16268            }
16269            self.advance();
16270            let which = self.advance();
16271            let action = self.parse_fk_action()?;
16272            match which {
16273                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
16274                    if seen_on_delete {
16275                        return Err(self.err("ON DELETE specified twice".into()));
16276                    }
16277                    seen_on_delete = true;
16278                    on_delete = action;
16279                }
16280                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
16281                    if seen_on_update {
16282                        return Err(self.err("ON UPDATE specified twice".into()));
16283                    }
16284                    seen_on_update = true;
16285                    on_update = action;
16286                }
16287                other => {
16288                    return Err(
16289                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
16290                    );
16291                }
16292            }
16293        }
16294        Ok((
16295            parent_table,
16296            parent_columns,
16297            on_delete,
16298            on_update,
16299            match_type,
16300            deferrable,
16301            initially_deferred,
16302        ))
16303    }
16304
16305    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
16306    /// NO ACTION`.
16307    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
16308        match self.advance() {
16309            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
16310            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
16311            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
16312                Token::Null => Ok(FkAction::SetNull),
16313                Token::Default => Ok(FkAction::SetDefault),
16314                other => Err(self.err(format!(
16315                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
16316                ))),
16317            },
16318            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
16319                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
16320                other => Err(self.err(format!(
16321                    "expected ACTION after NO in FK action, got {other:?}"
16322                ))),
16323            },
16324            other => Err(self.err(format!(
16325                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
16326            ))),
16327        }
16328    }
16329
16330    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
16331    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
16332    fn consume_if_not_exists(&mut self) -> bool {
16333        // `IF` arrives as a bare Ident (we don't reserve it because it
16334        // also appears mid-expression in PG, though we don't support
16335        // those forms yet).
16336        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16337        if !looks_like_if {
16338            return false;
16339        }
16340        // Peek one ahead before committing: only consume IF when it's
16341        // actually `IF NOT EXISTS`.
16342        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
16343            return false;
16344        }
16345        if !matches!(
16346            self.tokens.get(self.pos + 2),
16347            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16348        ) {
16349            return false;
16350        }
16351        self.advance(); // IF
16352        self.advance(); // NOT
16353        self.advance(); // EXISTS
16354        true
16355    }
16356
16357    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
16358    /// Consumes IF EXISTS as a pair; returns false otherwise
16359    /// without consuming any tokens.
16360    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
16361    /// ENABLE/DISABLE/FORCE/NO FORCE.
16362    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
16363        for kw in ["row", "level", "security"] {
16364            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
16365            {
16366                return Err(self.err(alloc::format!(
16367                    "expected {} in ROW LEVEL SECURITY, got {:?}",
16368                    kw.to_ascii_uppercase(),
16369                    self.peek()
16370                )));
16371            }
16372            self.advance();
16373        }
16374        Ok(())
16375    }
16376
16377    fn consume_if_exists(&mut self) -> bool {
16378        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16379        if !looks_like_if {
16380            return false;
16381        }
16382        if !matches!(
16383            self.tokens.get(self.pos + 1),
16384            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16385        ) {
16386            return false;
16387        }
16388        self.advance(); // IF
16389        self.advance(); // EXISTS
16390        true
16391    }
16392
16393    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16394    /// qualifiers after an index column ref. ASC / DESC are
16395    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16396    /// We accept and discard them since single-column BTree
16397    /// stores rows in natural key order today.
16398    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16399    /// ORDER BY key. Returns None when absent.
16400    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16401        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16402            return Ok(None);
16403        }
16404        self.advance();
16405        match self.advance() {
16406            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16407            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16408            other => Err(self.err(alloc::format!(
16409                "expected FIRST or LAST after NULLS, got {other:?}"
16410            ))),
16411        }
16412    }
16413
16414    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16415    /// rather than discarded.
16416    ///
16417    /// SPG's index does not scan in a direction — column ordering is
16418    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16419    /// reproduction of the DDL, and dropping the clause meant
16420    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16421    /// dump lost it, and a schema diff saw drift on every run.
16422    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16423        let mut order = crate::ast::IndexColumnOrder::default();
16424        loop {
16425            match self.peek() {
16426                Token::Asc => {
16427                    self.advance();
16428                }
16429                Token::Desc => {
16430                    order.descending = true;
16431                    self.advance();
16432                }
16433                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16434                    let look = self.tokens.get(self.pos + 1);
16435                    if matches!(
16436                        look,
16437                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16438                            || k.eq_ignore_ascii_case("last")
16439                    ) {
16440                        self.advance();
16441                        order.nulls_first = Some(matches!(
16442                            self.advance(),
16443                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
16444                        ));
16445                    } else {
16446                        break;
16447                    }
16448                }
16449                _ => break,
16450            }
16451        }
16452        order
16453    }
16454
16455    fn parse_create_index_stmt_after_create(
16456        &mut self,
16457        is_unique: bool,
16458    ) -> Result<Statement, ParseError> {
16459        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16460        debug_assert!(matches!(self.peek(), Token::Index));
16461        self.advance();
16462        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16463        // SPG's CREATE INDEX is synchronous end-to-end today (real
16464        // CONCURRENTLY variant with restartable scans queues with
16465        // v7.39 indexes epic), so the modifier has no runtime effect
16466        // — same accept-and-no-op shape as v7.37.16.5 DETACH
16467        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16468        // VIEW CONCURRENTLY.
16469        let mut concurrently = false;
16470        if matches!(
16471            self.peek(),
16472            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16473        ) {
16474            self.advance();
16475            concurrently = true;
16476        }
16477        let if_not_exists = self.consume_if_not_exists();
16478        // v7.39 (read01 round 93) — the index name is optional (PG since
16479        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16480        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16481        // was given; leave it empty and the engine derives a PG-style
16482        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16483        let name = if matches!(self.peek(), Token::On) {
16484            String::new()
16485        } else {
16486            self.expect_ident_like()?
16487        };
16488        if !matches!(self.peek(), Token::On) {
16489            return Err(self.err(format!(
16490                "expected ON after CREATE INDEX <name>, got {:?}",
16491                self.peek()
16492            )));
16493        }
16494        self.advance();
16495        let table = self.expect_ident_like()?;
16496        // Optional `USING <method>` — only recognised method in v2.0 is
16497        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16498        // ident `using` (we don't promote it to a reserved keyword
16499        // because it isn't reserved anywhere else in our SQL surface).
16500        let mut method_name: Option<String> = None;
16501        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16502            self.advance();
16503            let m = self.expect_ident_like()?;
16504            method_name = Some(m.to_ascii_lowercase());
16505            match m.to_ascii_lowercase().as_str() {
16506                "hnsw" => IndexMethod::Hnsw,
16507                "btree" => IndexMethod::BTree,
16508                "brin" => IndexMethod::Brin,
16509                // v7.12.3 — real GIN inverted index over `tsvector`.
16510                // v7.9.26b's `USING gin` → BTree silent fallback is
16511                // gone; the engine validates that the indexed column
16512                // is `tsvector` at CREATE INDEX time.
16513                "gin" => IndexMethod::Gin,
16514                // v7.9.26b — PG `pg_dump` emits `USING gist` /
16515                // `USING spgist` / `USING hash` for their built-in
16516                // AMs that SPG doesn't have a matching
16517                // implementation for; degrade to BTree on the
16518                // leading column so the schema loads + the index
16519                // catalogue stays consistent. Operator pays the
16520                // planner cost only for the queries that would have
16521                // used the specialised AM.
16522                "gist" | "spgist" | "hash" => IndexMethod::BTree,
16523                // v7.11.3 — pgvector ships both `ivfflat` and
16524                // `hnsw`. Customers shouldn't have to choose
16525                // their on-disk index method based on what SPG
16526                // implements; accept `ivfflat` as a synonym for
16527                // `hnsw` so PG schemas using either method drop
16528                // in. The vector distance op (`<->` / `<#>` /
16529                // `<=>`) at query time still picks the metric.
16530                "ivfflat" => IndexMethod::Hnsw,
16531                other => {
16532                    return Err(self.err(alloc::format!(
16533                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16534                    )));
16535                }
16536            }
16537        } else {
16538            IndexMethod::BTree
16539        };
16540        if !matches!(self.peek(), Token::LParen) {
16541            return Err(self.err(format!(
16542                "expected '(' before indexed column, got {:?}",
16543                self.peek()
16544            )));
16545        }
16546        self.advance();
16547        // v6.8.2 — accept either a bare column ident (legacy) or
16548        // an expression `fn(col, …)` for expression indexes.
16549        // Distinguish by peeking the token *after* the current
16550        // ident: `ident )` is the legacy column-only path;
16551        // anything else triggers the Pratt expression parser.
16552        // (`advance()` uses `mem::replace` to nil out the current
16553        // slot, so we can't save+rewind cleanly — peek-ahead via
16554        // direct index avoids the mutation.)
16555        let mut opclass: Option<String> = None;
16556        let mut key_collation: Option<String> = None;
16557        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16558            // Single column with `)` immediately after — fast path.
16559            // v7.9.29 — also: bare column followed by `,` (the
16560            // multi-column form `(a, b, c)`). Without this branch
16561            // the leading ident gets pulled into `parse_expr`
16562            // which then sets `expression = Some(Column(a))` and
16563            // breaks Display round-trip on the multi-column shape.
16564            Token::Ident(s) | Token::QuotedIdent(s)
16565                if matches!(
16566                    self.tokens.get(self.pos + 1),
16567                    Some(Token::RParen | Token::Comma)
16568                ) =>
16569            {
16570                self.advance();
16571                (s, None)
16572            }
16573            // v7.9.22 — single column followed by a pgvector
16574            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16575            // v7.15.0 — capture the opclass instead of discarding
16576            // it so the engine can dispatch (e.g. `gin_trgm_ops`
16577            // → real trigram-shingle GIN over a TEXT column).
16578            // Vector/HNSW opclasses still take their distance
16579            // metric from the query operator (`<->` / `<#>` /
16580            // `<=>`), so for those callers the opclass stays
16581            // informational.
16582            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16583            // opclass: `(embedding public.vector_cosine_ops)`. Strip
16584            // the schema and dispatch on the bare opclass, the same
16585            // treatment table/type names get.
16586            Token::Ident(s) | Token::QuotedIdent(s)
16587                if matches!(
16588                    self.tokens.get(self.pos + 1),
16589                    Some(Token::Ident(_) | Token::QuotedIdent(_))
16590                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16591                    && matches!(
16592                        self.tokens.get(self.pos + 3),
16593                        Some(Token::Ident(op) | Token::QuotedIdent(op))
16594                            if is_vector_opclass_name(op)
16595                    ) =>
16596            {
16597                self.advance(); // column name
16598                self.advance(); // schema qualifier
16599                self.advance(); // dot
16600                let op_tok = self.advance();
16601                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16602                    opclass = Some(op.to_ascii_lowercase());
16603                }
16604                (s, None)
16605            }
16606            // r1038 — an operator class is recognised by its POSITION, not
16607            // by a list of names. It used to be `is_vector_opclass_name`,
16608            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16609            // sentori's migration wrote — was a syntax error while
16610            // `USING gin (doc)` parsed. Anything sitting between a column
16611            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16612            // two bare identifiers in a row are not valid there otherwise.
16613            Token::Ident(s) | Token::QuotedIdent(s)
16614                if matches!(
16615                    self.tokens.get(self.pos + 1),
16616                    Some(Token::Ident(op) | Token::QuotedIdent(op))
16617                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
16618                            self.tokens.get(self.pos + 2)
16619                        )
16620                ) =>
16621            {
16622                self.advance(); // column name
16623                // Capture the opclass token, lower-cased for
16624                // case-insensitive engine dispatch.
16625                let op_tok = self.advance();
16626                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16627                    opclass = Some(op.to_ascii_lowercase());
16628                }
16629                (s, None)
16630            }
16631            Token::Ident(_) | Token::QuotedIdent(_) => {
16632                // v7.39 (round 538) — an explicit COLLATE on the key,
16633                // read by LOOKAHEAD because `parse_expr` absorbs the
16634                // clause as a no-op (SPG orders text by bytes, which is
16635                // the C collation, so it changes nothing to honour). PG
16636                // still PRINTS it: an explicitly written `"C"` and the
16637                // collation a column inherits are different collation
16638                // OBJECTS even where they sort identically, which is why
16639                // `(a COLLATE "C")` shows on a C-collation database too.
16640                if matches!(
16641                    self.tokens.get(self.pos + 1),
16642                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16643                ) {
16644                    key_collation = match self.tokens.get(self.pos + 2) {
16645                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16646                            Some(n.clone())
16647                        }
16648                        _ => None,
16649                    };
16650                }
16651                // v7.39.2 — the clause is read by the LOOKAHEAD above and
16652                // belongs to the KEY, not to the expression. Since
16653                // `COLLATE` became a node, letting `parse_expr` build one
16654                // here put the collation in twice and the key deparsed as
16655                // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16656                // is the same idea and already exists, so this borrows it:
16657                // absorb into the side channel, and the key's own
16658                // lookahead is what carries it.
16659                // v7.39.2 — and the key can only CARRY the byte-order
16660                // spellings. Absorbing into the side channel accepts any
16661                // name, so suppressing the node here without this check
16662                // silently accepted `(name COLLATE "en_US")`, which SPG's
16663                // index cannot honour — a refusal that was doing real
16664                // work, removed by the suppression and put back here.
16665                if let Some(name) = &key_collation {
16666                    let lc = name.to_ascii_lowercase();
16667                    let byte_order = matches!(
16668                        lc.as_str(),
16669                        "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16670                    );
16671                    let mysql_ok = self.mysql_dialect
16672                        && (lc.ends_with("_ci")
16673                            || lc.ends_with("_bin")
16674                            || lc == "binary"
16675                            || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16676                    if !byte_order && !mysql_ok {
16677                        return Err(self.err(alloc::format!(
16678                            "COLLATE {name:?} is not supported in this position: an index \
16679                             key carries the byte-order spellings only. Declare it on the \
16680                             column (`x text COLLATE {name:?}`) instead"
16681                        )));
16682                    }
16683                }
16684                let saved_key_ctx = self.in_order_by_key;
16685                self.in_order_by_key = true;
16686                let key_expr = self.parse_expr(0);
16687                self.in_order_by_key = saved_key_ctx;
16688                let key_expr = key_expr?;
16689                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16690                    self.err("expression index key must reference at least one column".into())
16691                })?;
16692                (primary, Some(key_expr))
16693            }
16694            // v7.37.43-T4 — parenthesised expression index key
16695            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16696            // PG's CREATE INDEX requires the expression to be in
16697            // its own parens to disambiguate function calls from
16698            // column lists, so this `LParen` is the inner open-paren
16699            // of an expression key. parse_expr handles the recursive
16700            // descent and consumes the matching `RParen`.
16701            Token::LParen => {
16702                let key_expr = self.parse_expr(0)?;
16703                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16704                    self.err("expression index key must reference at least one column".into())
16705                })?;
16706                (primary, Some(key_expr))
16707            }
16708            other => {
16709                return Err(self.err(format!(
16710                    "expected column ident or expression, got {other:?}"
16711                )));
16712            }
16713        };
16714        // v7.9.14 — accept extra comma-separated columns inside
16715        // the index key parens (`CREATE INDEX … (a, b, c)`).
16716        // mailrs F2.
16717        //
16718        // v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
16719        // / `NULLS LAST` is KEPT. It used to be parsed and dropped on
16720        // the floor, so `CREATE INDEX i ON t (a, b DESC)` read back from
16721        // `pg_get_indexdef` as `(a, b)`: a dump lost the clause and a
16722        // schema diff saw drift on every run. Reported by sentori
16723        // against 7.39.10, and the same defect round 537 fixed for the
16724        // LEADING column, in the loop right beside it.
16725        let mut extra_columns: Vec<String> = Vec::new();
16726        let mut extra_orders: Vec<crate::ast::IndexColumnOrder> = Vec::new();
16727        // The leading column may also have ASC/DESC after it — and that
16728        // one is the column SPG indexes, so its clause is kept.
16729        let key_order = self.consume_optional_index_column_qualifiers();
16730        while matches!(self.peek(), Token::Comma) {
16731            self.advance();
16732            let extra = self.expect_ident_like()?;
16733            extra_orders.push(self.consume_optional_index_column_qualifiers());
16734            extra_columns.push(extra);
16735        }
16736        if !matches!(self.peek(), Token::RParen) {
16737            return Err(self.err(format!(
16738                "expected ')' after indexed column / expression, got {:?}",
16739                self.peek()
16740            )));
16741        }
16742        self.advance();
16743        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16744        // index-only-scan annotation. Bare ident (not a reserved
16745        // keyword) so we test by case-insensitive string match.
16746        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16747        {
16748            self.advance();
16749            if !matches!(self.peek(), Token::LParen) {
16750                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16751            }
16752            self.advance();
16753            let mut cols = Vec::new();
16754            loop {
16755                cols.push(self.expect_ident_like()?);
16756                match self.peek() {
16757                    Token::Comma => {
16758                        self.advance();
16759                    }
16760                    Token::RParen => {
16761                        self.advance();
16762                        break;
16763                    }
16764                    other => {
16765                        return Err(self.err(format!(
16766                            "expected ',' or ')' in INCLUDE list, got {other:?}"
16767                        )));
16768                    }
16769                }
16770            }
16771            cols
16772        } else {
16773            Vec::new()
16774        };
16775        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16776        // storage parameters. pgvector emits `WITH (lists = N)` for
16777        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16778        // SPG's HNSW picks its own parameters today (tunable via
16779        // env vars), so the WITH clause is informational and dropped.
16780        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16781            self.advance();
16782            if !matches!(self.peek(), Token::LParen) {
16783                return Err(self.err(format!(
16784                    "expected '(' after WITH in CREATE INDEX, got {:?}",
16785                    self.peek()
16786                )));
16787            }
16788            self.advance();
16789            loop {
16790                if matches!(self.peek(), Token::RParen) {
16791                    self.advance();
16792                    break;
16793                }
16794                // Drain `key = value` or bare `key` tokens.
16795                let _ = self.advance(); // key
16796                if matches!(self.peek(), Token::Eq) {
16797                    self.advance();
16798                    let _ = self.advance(); // value (int / string / ident)
16799                }
16800                match self.peek() {
16801                    Token::Comma => {
16802                        self.advance();
16803                    }
16804                    Token::RParen => {
16805                        self.advance();
16806                        break;
16807                    }
16808                    other => {
16809                        return Err(self.err(format!(
16810                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
16811                        )));
16812                    }
16813                }
16814            }
16815        }
16816        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16817        // which sits between the key list and the WHERE clause.
16818        let mut nulls_not_distinct = false;
16819        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16820            let n1 = self.tokens.get(self.pos + 1);
16821            let n2 = self.tokens.get(self.pos + 2);
16822            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16823                self.advance(); // NULLS
16824                self.advance(); // NOT
16825                self.advance(); // DISTINCT
16826                nulls_not_distinct = true;
16827            } else if matches!(n1, Some(Token::Distinct)) {
16828                self.advance(); // NULLS
16829                self.advance(); // DISTINCT
16830            }
16831        }
16832        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16833        let partial_predicate = if matches!(self.peek(), Token::Where) {
16834            self.advance();
16835            Some(self.parse_expr(0)?)
16836        } else {
16837            None
16838        };
16839        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16840        // sense: uniqueness over an ANN structure has no clean
16841        // semantics. Reject early. (BRIN UNIQUE is similarly
16842        // meaningless — block both.)
16843        if is_unique && !matches!(method, IndexMethod::BTree) {
16844            return Err(self.err(alloc::format!(
16845                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16846                method
16847            )));
16848        }
16849        Ok(Statement::CreateIndex(CreateIndexStatement {
16850            concurrently,
16851            name,
16852            key_order,
16853            key_collation,
16854            table,
16855            column,
16856            nulls_not_distinct,
16857            method,
16858            if_not_exists,
16859            included_columns,
16860            partial_predicate,
16861            extra_columns: extra_columns.clone(),
16862            extra_orders: extra_orders.clone(),
16863            expression,
16864            is_unique,
16865            opclass,
16866            method_name,
16867        }))
16868    }
16869
16870    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16871    /// column-level `REFERENCES ...` clause. The trailing FK is
16872    /// normalised into table-level shape (single-element columns +
16873    /// parent_columns) so the engine sees one uniform constraint list.
16874    fn parse_column_def_with_fk(
16875        &mut self,
16876    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16877        let col = self.parse_column_def()?;
16878        // v7.39 (round 308, V29) — an explicitly named inline FK:
16879        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16880        // loop leaves this spelling intact precisely so the name can be
16881        // kept here; PG reports it in violation messages and matches it
16882        // in `SET CONSTRAINTS`.
16883        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16884        {
16885            self.advance();
16886            Some(self.expect_ident_like()?)
16887        } else {
16888            None
16889        };
16890        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16891        let inline_references = matches!(
16892            self.peek(),
16893            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16894        );
16895        if !inline_references {
16896            return Ok((col, None));
16897        }
16898        let (
16899            parent_table,
16900            parent_columns,
16901            on_delete,
16902            on_update,
16903            match_type,
16904            deferrable,
16905            initially_deferred,
16906        ) = self.parse_references_tail(1)?;
16907        let fk = ForeignKeyConstraint {
16908            name: declared_name,
16909            columns: vec![col.name.clone()],
16910            parent_table,
16911            parent_columns,
16912            on_delete,
16913            on_update,
16914            match_type,
16915            deferrable,
16916            initially_deferred,
16917        };
16918        Ok((col, Some(fk)))
16919    }
16920
16921    /// v7.13.0 — parse a column type (consuming the type ident and
16922    /// any trailing parameters / `[]`), without surrounding column
16923    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16924    /// Returns the resolved `ColumnTypeName` plus implied
16925    /// `(auto_increment, not_null)` flags from PG SERIAL family
16926    /// shorthands — callers that don't expect those (ALTER COLUMN
16927    /// TYPE) can discard them.
16928    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16929        let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16930        Ok(ty)
16931    }
16932
16933    #[allow(clippy::type_complexity)]
16934    fn parse_type_with_implied_flags(
16935        &mut self,
16936    ) -> Result<
16937        (
16938            ColumnTypeName,
16939            bool,
16940            bool,
16941            Option<String>,
16942            Collation,
16943            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16944            bool,
16945            // v7.39 (round 676) — the collation NAME as written, which the
16946            // `Collation` enum above cannot carry.
16947            Option<String>,
16948            bool,
16949            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16950            // list captured at type-parse time. None for all
16951            // non-ENUM types.
16952            Option<Vec<String>>,
16953            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16954            // list. Distinct from ENUM (subset semantics).
16955            Option<Vec<String>>,
16956            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
16957            // width, lost when the type collapses to SmallInt / Int.
16958            Option<MysqlIntWidth>,
16959            // v7.39 (round 424) — declared fractional-seconds precision of a
16960            // MySQL temporal column (bare spelling = 0). None under PG.
16961            Option<u8>,
16962            // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
16963            // two are different types on MySQL and SPG stores both as
16964            // `Timestamp`, so the spelling has to travel separately or
16965            // a dump silently rewrites the column.
16966            bool,
16967            // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
16968            // display hint: it rounds on write.
16969            Option<(u8, u8)>,
16970        ),
16971        ParseError,
16972    > {
16973        let mut ty_ident = match self.advance() {
16974            Token::Ident(s) => s,
16975            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
16976            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
16977            // '<span>'` literal grammar. As a column type it lands
16978            // here directly; downstream resolution still uses the
16979            // canonical lowercase string.
16980            Token::Interval => "interval".to_string(),
16981            other => {
16982                return Err(ParseError {
16983                    message: format!("expected column type, got {other:?}"),
16984                    token_pos: self.consumed_pos(),
16985                });
16986            }
16987        };
16988        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
16989        // pg_dump qualifies extension types (`public.vector(1024)`).
16990        // SPG is single-namespace; drop the schema and resolve the
16991        // bare type — same treatment table names already get.
16992        while matches!(self.peek(), Token::Dot) {
16993            self.advance();
16994            ty_ident = self.expect_ident_like()?;
16995        }
16996        let mut implied_auto_increment = false;
16997        let mut implied_not_null = false;
16998        let mut user_type_ref: Option<String> = None;
16999        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
17000        // value list, captured here and bubbled up through the
17001        // ColumnDef so the engine can attach it to the column
17002        // schema (and validate INSERT cells against it).
17003        let mut inline_enum_variants: Option<Vec<String>> = None;
17004        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
17005        let mut inline_set_variants: Option<Vec<String>> = None;
17006        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
17007        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
17008        // collapses to SmallInt / Int. Only under the MySQL dialect.
17009        let mut mysql_int_width: Option<MysqlIntWidth> = None;
17010        // v7.39 (round 424) — the declared fractional-seconds precision of a
17011        // MySQL temporal column. Set by the temporal arms below; stays None
17012        // for PG (whose temporal columns keep full microseconds).
17013        let mut mysql_fsp: Option<u8> = None;
17014        let mut mysql_declared_timestamp = false;
17015        let mut mysql_float_md: Option<(u8, u8)> = None;
17016        let mut ty = match ty_ident.as_str() {
17017            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
17018            "smallserial" | "serial2" => {
17019                implied_auto_increment = true;
17020                implied_not_null = true;
17021                ColumnTypeName::SmallInt
17022            }
17023            "serial" | "serial4" => {
17024                implied_auto_increment = true;
17025                implied_not_null = true;
17026                ColumnTypeName::Int
17027            }
17028            "bigserial" | "serial8" => {
17029                implied_auto_increment = true;
17030                implied_not_null = true;
17031                ColumnTypeName::BigInt
17032            }
17033            // MySQL flavours we accept by aliasing to the closest SPG
17034            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
17035            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
17036            // 24-bit) → INT. UNSIGNED modifiers are consumed below
17037            // without semantic effect.
17038            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
17039            // PG's internal type names; pg_dump and hand-written PG schemas
17040            // use them interchangeably with smallint / int / bigint (the cast
17041            // path already accepted them, only the column grammar didn't).
17042            "smallint" | "int2" => {
17043                // v7.14.0 — MySQL display-width on integers
17044                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
17045                // parenthesised number is purely cosmetic — it
17046                // doesn't change storage. Accept + discard.
17047                self.consume_optional_paren_size();
17048                ColumnTypeName::SmallInt
17049            }
17050            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
17051            // canonical encoding for BOOLEAN. Every MySQL driver
17052            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
17053            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
17054            // 4.3 SPG classified TINYINT(1) as SmallInt, which
17055            // gave the customer i16-shaped values where the app
17056            // expected bool — a Tier-A silent type drift on
17057            // mysqldump restores. Now: `TINYINT(1)` → Bool;
17058            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
17059            // stay SmallInt (the legacy width-agnostic path).
17060            "tinyint" => {
17061                let width = self.peek_optional_paren_size_value();
17062                self.consume_optional_paren_size();
17063                if width == Some(1) {
17064                    ColumnTypeName::Bool
17065                } else {
17066                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
17067                    // lost width so the write path can enforce -128..127.
17068                    if self.mysql_dialect {
17069                        mysql_int_width = Some(MysqlIntWidth::Tiny);
17070                    }
17071                    ColumnTypeName::SmallInt
17072                }
17073            }
17074            "mediumint" => {
17075                self.consume_optional_paren_size();
17076                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
17077                if self.mysql_dialect {
17078                    mysql_int_width = Some(MysqlIntWidth::Medium);
17079                }
17080                ColumnTypeName::Int
17081            }
17082            "int" | "integer" | "int4" => {
17083                self.consume_optional_paren_size();
17084                ColumnTypeName::Int
17085            }
17086            "bigint" | "int8" => {
17087                self.consume_optional_paren_size();
17088                ColumnTypeName::BigInt
17089            }
17090            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
17091            // (mailrs round-5 G6). Consume the optional `PRECISION`
17092            // tail when the type keyword was `double` / `DOUBLE`.
17093            //
17094            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
17095            // FLOAT". `FLOAT(p)` picks the width the way PG does:
17096            // p in 1..=24 is real, 25..=53 is double precision, and
17097            // anything else is an error.
17098            "float" | "double" | "real" => {
17099                if ty_ident.eq_ignore_ascii_case("double")
17100                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
17101                {
17102                    self.advance();
17103                }
17104                if ty_ident.eq_ignore_ascii_case("real") {
17105                    // v7.39 (round 274) — the two dialects genuinely
17106                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
17107                    // synonym for DOUBLE (8-byte). Round 269 made REAL
17108                    // 32-bit globally and thereby narrowed the stored
17109                    // precision of every MySQL REAL column.
17110                    if self.mysql_dialect {
17111                        ColumnTypeName::Float
17112                    } else {
17113                        ColumnTypeName::Real
17114                    }
17115                } else if self.mysql_dialect
17116                    && matches!(self.peek(), Token::LParen)
17117                    && self.peek_paren_has_comma()
17118                {
17119                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
17120                    // display form (`FLOAT(10,2)`), which PG has no
17121                    // equivalent of. It was `syntax error at or near ","`,
17122                    // so the whole CREATE failed.
17123                    //
17124                    // v7.39.2 — the guard said `float` while the comment
17125                    // said both, so `DOUBLE(10,2)` — which every legacy
17126                    // MySQL schema uses for money — still failed the
17127                    // whole CREATE with `syntax error at or near "("`.
17128                    // Measured on 9.7.2: both forms are accepted, and the
17129                    // digits are NOT a display hint, they round on write
17130                    // (3.14159265358979 into either stores 3.14). The
17131                    // rounding is recorded as a residual; accepting the
17132                    // syntax and keeping the width is the half this
17133                    // change makes.
17134                    // v7.39.3 — keep the pair. The digits are not a
17135                    // display hint: MySQL 9.7.2 ROUNDS on write and
17136                    // refuses a value wider than `m` (errno 1264), so a
17137                    // column declared for money held more precision here
17138                    // than its schema said.
17139                    let (m, d) = self.parse_optional_numeric_params()?;
17140                    mysql_float_md = Some((
17141                        u8::try_from(m).unwrap_or(u8::MAX),
17142                        u8::try_from(d.max(0)).unwrap_or(u8::MAX),
17143                    ));
17144                    if ty_ident.eq_ignore_ascii_case("float") {
17145                        ColumnTypeName::Real
17146                    } else {
17147                        ColumnTypeName::Float
17148                    }
17149                } else if ty_ident.eq_ignore_ascii_case("float")
17150                    && matches!(self.peek(), Token::LParen)
17151                {
17152                    // PG words the two bounds differently, and
17153                    // parse_paren_size already rejects a zero.
17154                    let p = self.parse_paren_size("FLOAT")?;
17155                    if p > 53 {
17156                        return Err(self.err(String::from(
17157                            "precision for type float must be less than 54 bits",
17158                        )));
17159                    }
17160                    if p <= 24 {
17161                        ColumnTypeName::Real
17162                    } else {
17163                        ColumnTypeName::Float
17164                    }
17165                } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
17166                    // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
17167                    // eight (it is `float8`'s spelling there). SPG used
17168                    // PG's for both, so a MySQL FLOAT column silently
17169                    // kept more precision than MySQL does — measured,
17170                    // 3.14159265358979 comes back as 3.14159 there and
17171                    // came back whole here — and reported itself as
17172                    // `double` to every reflection.
17173                    //
17174                    // This is the mirror of the REAL split above: the
17175                    // two dialects disagree about which spelling means
17176                    // which width, and one of them was already honoured.
17177                    ColumnTypeName::Real
17178                } else {
17179                    ColumnTypeName::Float
17180                }
17181            }
17182            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
17183            "float4" => ColumnTypeName::Real,
17184            "float8" => ColumnTypeName::Float,
17185            "text" => ColumnTypeName::Text,
17186            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
17187            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
17188            // real MySQL schema and NONE of them existed: the CREATE
17189            // failed outright with `type "blob" does not exist`, so the
17190            // table was never made. The sizes differ only in MySQL's
17191            // maximum length, which SPG does not cap, so they collapse
17192            // onto TEXT and BYTEA the way the unsized spellings do.
17193            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
17194            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
17195            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
17196            // enforce, consumed so the declaration parses.
17197            "varbinary" | "binary" => {
17198                self.consume_optional_paren_size();
17199                ColumnTypeName::Bytes
17200            }
17201            "name" => ColumnTypeName::Name,
17202            "xid" => ColumnTypeName::Xid,
17203            "oid" => ColumnTypeName::Oid,
17204            "xid8" => ColumnTypeName::Xid8,
17205            "bool" | "boolean" => ColumnTypeName::Bool,
17206            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
17207            // an unbounded `character varying`, which the arm below has always
17208            // read as text. Only the short spelling demanded a length, so
17209            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
17210            // there is — failed on `VARCHAR type requires (N)` while the long
17211            // spelling of the same thing was accepted. The same asymmetry
17212            // round 613 closed on the CAST side, here on the DDL side.
17213            "varchar" => {
17214                if matches!(self.peek(), Token::LParen) {
17215                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17216                } else {
17217                    ColumnTypeName::Text
17218                }
17219            }
17220            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
17221            // `character` below (SQL standard).
17222            "char" => {
17223                if matches!(self.peek(), Token::LParen) {
17224                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17225                } else {
17226                    ColumnTypeName::Char(1)
17227                }
17228            }
17229            // pg_dump's canonical spellings: `character varying(n)` = varchar,
17230            // `character(n)` = char, bare `character` = char(1). Unbounded
17231            // `character varying` maps to text.
17232            "character" => {
17233                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
17234                    self.advance();
17235                    if matches!(self.peek(), Token::LParen) {
17236                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17237                    } else {
17238                        ColumnTypeName::Text
17239                    }
17240                } else if matches!(self.peek(), Token::LParen) {
17241                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17242                } else {
17243                    ColumnTypeName::Char(1)
17244                }
17245            }
17246            "vector" => {
17247                let dim = self.parse_paren_size("VECTOR")?;
17248                let encoding = self.parse_optional_vector_encoding()?;
17249                ColumnTypeName::Vector { dim, encoding }
17250            }
17251            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
17252            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
17253            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
17254            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
17255            // DECIMAL(10,2))` — how nearly every money column is written,
17256            // in either dialect — was a syntax error and the table was
17257            // never created. `FIXED` is MySQL's alias alone, so it is
17258            // taken only in that dialect.
17259            "numeric" | "decimal" | "dec" => {
17260                let (precision, scale) = self.parse_optional_numeric_params()?;
17261                ColumnTypeName::Numeric(precision, scale)
17262            }
17263            "fixed" if self.mysql_dialect => {
17264                let (precision, scale) = self.parse_optional_numeric_params()?;
17265                ColumnTypeName::Numeric(precision, scale)
17266            }
17267            "date" => ColumnTypeName::Date,
17268            // MySQL's `DATETIME` is the same domain as standard
17269            // `TIMESTAMP` — accept both spellings.
17270            "timestamp" | "datetime" => {
17271                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
17272                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
17273                // TIME ZONE` clause, so consume it first.
17274                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
17275                // (it truncates on write and pads on render), so capture it;
17276                // a bare spelling means precision 0 there. PG stores µs always
17277                // and keeps `None`.
17278                let n = self.take_optional_paren_size();
17279                if self.mysql_dialect {
17280                    mysql_fsp = Some(n.unwrap_or(0).min(6));
17281                    // v7.39.2 — remember WHICH spelling was written.
17282                    // MySQL and MariaDB keep `timestamp` and `datetime`
17283                    // apart everywhere a client can read the type back,
17284                    // and SPG reported `datetime` for both — so a dump
17285                    // and reload silently changed the column's declared
17286                    // type, and MySQL's TIMESTAMP is not DATETIME (a
17287                    // different range, and UTC conversion on the way in
17288                    // and out).
17289                    mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
17290                }
17291                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
17292                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
17293                // the full form. SPG canonicalises:
17294                //   - WITH TIME ZONE    → Timestamptz
17295                //   - WITHOUT TIME ZONE → Timestamp
17296                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17297                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17298                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17299                {
17300                    self.advance(); // WITH
17301                    self.advance(); // TIME
17302                    self.advance(); // ZONE
17303                    ColumnTypeName::Timestamptz
17304                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17305                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17306                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17307                {
17308                    self.advance(); // WITHOUT
17309                    self.advance(); // TIME
17310                    self.advance(); // ZONE
17311                    ColumnTypeName::Timestamp
17312                } else {
17313                    // A second `(precision)` cannot legally follow, but the
17314                    // old grammar tolerated it; keep that tolerance.
17315                    self.consume_optional_paren_size();
17316                    ColumnTypeName::Timestamp
17317                }
17318            }
17319            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
17320            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
17321            // only PG-wire OID differs.
17322            "timestamptz" => {
17323                self.consume_optional_paren_size();
17324                ColumnTypeName::Timestamptz
17325            }
17326            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
17327            // validation. We accept the JSONB spelling too because
17328            // most PG clients default to it; SPG doesn't distinguish
17329            // the two (no path-operator perf advantage to model).
17330            "json" => ColumnTypeName::Json,
17331            "jsonb" => ColumnTypeName::Jsonb,
17332            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
17333            // surface here. Same storage shape; mapping happens at
17334            // the engine side via the ColumnTypeName → DataType
17335            // resolver. Literal forms are handled at coerce_value
17336            // time so the lexer stays untouched.
17337            "bytea" | "bytes" => ColumnTypeName::Bytes,
17338            // v7.17.0 Phase 7 — PG network address types
17339            // v7.17.0 had a Text-backed fallback here for
17340            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
17341            // each to a first-class type; the keywords are
17342            // bound below in the ζ-A block.
17343            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
17344            // The actual `to_tsvector` / `@@` / `ts_rank` surface
17345            // arrives in v7.12.1+; the type itself loads here so
17346            // mailrs's `scripts/init-schema.sql` runs unmodified.
17347            "tsvector" => ColumnTypeName::TsVector,
17348            "tsquery" => ColumnTypeName::TsQuery,
17349            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
17350            // surface for Django / Rails / Hibernate's default
17351            // PK pattern.
17352            "uuid" => ColumnTypeName::Uuid,
17353            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
17354            // Storage = three-field {months, days, micros}, catalog
17355            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
17356            // line `INTERVAL` was parser-rejected at CREATE TABLE.
17357            "interval" => {
17358                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
17359                // SECOND` and an optional `(p)` precision. SPG stores the full
17360                // {months,days,micros}; consume + ignore the qualifier/precision.
17361                while matches!(self.peek(), Token::To)
17362                    || matches!(self.peek(), Token::Ident(s) if matches!(
17363                        s.to_ascii_lowercase().as_str(),
17364                        "year" | "month" | "day" | "hour" | "minute" | "second"
17365                    ))
17366                {
17367                    self.advance();
17368                }
17369                self.consume_optional_paren_size();
17370                ColumnTypeName::Interval
17371            }
17372            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
17373            // i64 microseconds since 00:00:00. Wire OID 1083.
17374            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
17375            "time" => {
17376                // v7.39 (round 424) — MySQL TIME carries a semantic
17377                // fractional-seconds precision, bare meaning 0.
17378                let n = self.take_optional_paren_size();
17379                if self.mysql_dialect {
17380                    mysql_fsp = Some(n.unwrap_or(0).min(6));
17381                }
17382                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17383                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17384                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17385                {
17386                    self.advance();
17387                    self.advance();
17388                    self.advance();
17389                    ColumnTypeName::TimeTz
17390                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17391                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17392                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17393                {
17394                    self.advance();
17395                    self.advance();
17396                    self.advance();
17397                    ColumnTypeName::Time
17398                } else {
17399                    ColumnTypeName::Time
17400                }
17401            }
17402            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17403            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17404            "year" => ColumnTypeName::Year,
17405            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17406            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17407            "timetz" => ColumnTypeName::TimeTz,
17408            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17409            // Wire OID 790.
17410            "money" => ColumnTypeName::Money,
17411            // v7.17.0 Phase 3.P0-38 — PG range types.
17412            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17413            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17414            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17415            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17416            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17417            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17418            // v7.37.5 δ — PG 14+ multirange keywords.
17419            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17420            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17421            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17422            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17423            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17424            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17425            // v7.37.5 ε — PG geometry scalar keywords.
17426            "point" => ColumnTypeName::Point,
17427            "lseg" => ColumnTypeName::Lseg,
17428            "path" => ColumnTypeName::Path,
17429            "box" => ColumnTypeName::PgBox,
17430            "polygon" => ColumnTypeName::Polygon,
17431            "line" => ColumnTypeName::Line,
17432            "circle" => ColumnTypeName::Circle,
17433            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17434            "inet" => ColumnTypeName::Inet,
17435            "cidr" => ColumnTypeName::Cidr,
17436            "macaddr" => ColumnTypeName::Macaddr,
17437            "macaddr8" => ColumnTypeName::Macaddr8,
17438            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17439            // width in the value, so the optional `(N)` typmod is accepted and
17440            // ignored (the column stores whatever width it's given).
17441            "bit" => {
17442                let varying = matches!(
17443                    self.peek(),
17444                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17445                );
17446                if varying {
17447                    self.advance();
17448                }
17449                // v7.39 (round 281) — the length used to be parsed and
17450                // dropped, so `bit(3)` accepted a five-bit string.
17451                let n = if matches!(self.peek(), Token::LParen) {
17452                    self.parse_paren_size("BIT")?
17453                } else {
17454                    0
17455                };
17456                if varying {
17457                    ColumnTypeName::BitVarying(n)
17458                } else {
17459                    ColumnTypeName::Bit(n)
17460                }
17461            }
17462            "varbit" => {
17463                let n = if matches!(self.peek(), Token::LParen) {
17464                    self.parse_paren_size("VARBIT")?
17465                } else {
17466                    0
17467                };
17468                ColumnTypeName::BitVarying(n)
17469            }
17470            "xml" => ColumnTypeName::Xml,
17471            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17472            "hstore" => ColumnTypeName::Hstore,
17473            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17474            // `ENUM('a','b','c')`. Storage is TEXT; the value
17475            // list lands on `inline_enum_variants` for the
17476            // engine to validate INSERT cells against. Empty
17477            // value list is a parse error (matches MySQL).
17478            "enum" => {
17479                // Expect the opening `(`.
17480                if !matches!(self.peek(), Token::LParen) {
17481                    return Err(self.err(alloc::format!(
17482                        "expected '(' after ENUM, got {:?}",
17483                        self.peek()
17484                    )));
17485                }
17486                self.advance();
17487                let mut variants: Vec<String> = Vec::new();
17488                loop {
17489                    match self.advance() {
17490                        Token::String(s) => variants.push(s),
17491                        other => {
17492                            return Err(self.err(alloc::format!(
17493                                "ENUM(...) expects string literal variants, got {other:?}"
17494                            )));
17495                        }
17496                    }
17497                    match self.peek() {
17498                        Token::Comma => {
17499                            self.advance();
17500                            continue;
17501                        }
17502                        Token::RParen => {
17503                            self.advance();
17504                            break;
17505                        }
17506                        other => {
17507                            return Err(self.err(alloc::format!(
17508                                "expected ',' or ')' in ENUM(...), got {other:?}"
17509                            )));
17510                        }
17511                    }
17512                }
17513                if variants.is_empty() {
17514                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
17515                }
17516                inline_enum_variants = Some(variants);
17517                // Storage is plain TEXT; the variant list lives on
17518                // the ColumnSchema side.
17519                ColumnTypeName::Text
17520            }
17521            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17522            // `SET('a','b','c')`. Same parse shape as ENUM;
17523            // semantics differ (subset rather than pick-one).
17524            "set" => {
17525                if !matches!(self.peek(), Token::LParen) {
17526                    return Err(self.err(alloc::format!(
17527                        "expected '(' after SET, got {:?}",
17528                        self.peek()
17529                    )));
17530                }
17531                self.advance();
17532                let mut variants: Vec<String> = Vec::new();
17533                loop {
17534                    match self.advance() {
17535                        Token::String(s) => variants.push(s),
17536                        other => {
17537                            return Err(self.err(alloc::format!(
17538                                "SET(...) expects string literal variants, got {other:?}"
17539                            )));
17540                        }
17541                    }
17542                    match self.peek() {
17543                        Token::Comma => {
17544                            self.advance();
17545                            continue;
17546                        }
17547                        Token::RParen => {
17548                            self.advance();
17549                            break;
17550                        }
17551                        other => {
17552                            return Err(self.err(alloc::format!(
17553                                "expected ',' or ')' in SET(...), got {other:?}"
17554                            )));
17555                        }
17556                    }
17557                }
17558                if variants.is_empty() {
17559                    return Err(self.err("SET(...) must declare at least one variant".into()));
17560                }
17561                inline_set_variants = Some(variants);
17562                ColumnTypeName::Text
17563            }
17564            _other => {
17565                // v7.17.0 Phase 1.4 — unknown ident → defer
17566                // resolution to the engine. Stored as Text in
17567                // ColumnTypeName + the original name carried as
17568                // `user_type_ref` so CREATE TABLE can look up
17569                // user-defined enum / domain types.
17570                user_type_ref = Some(ty_ident.clone());
17571                ColumnTypeName::Text
17572            }
17573        };
17574        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17575        // right after the type keyword. Pre-4.4 SPG consumed +
17576        // discarded the keyword, leaving a customer column
17577        // declared `id INT UNSIGNED NOT NULL` silently accepting
17578        // negative values — a Tier-A correctness drift where
17579        // application invariants (auto-increment-IDs never
17580        // negative) silently broke on cutover. Now: capture as
17581        // a column flag, persist on the schema, enforce at
17582        // INSERT / UPDATE time.
17583        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17584        {
17585            self.advance();
17586            true
17587        } else {
17588            false
17589        };
17590        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17591        // `<type> COLLATE <name>` post-fixes on text columns. SPG
17592        // stores text as UTF-8 always so CHARACTER SET is still a
17593        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17594        // name: it gets classified into a `Collation` variant the
17595        // engine consults at WHERE-eval time. PG `default` /
17596        // `pg_catalog.default` / `C` / `POSIX` collations all
17597        // resolve to `Binary` (the prior behaviour); `_ci` /
17598        // `case_insensitive` / `nocase` shift to CaseInsensitive.
17599        // The schema-qualifier form (`pg_catalog.default`) lexes
17600        // as `Ident '.' Ident` — peek for the `.` and consume both
17601        // halves so it's treated as one collation name. PG's
17602        // `IDENT.IDENT` collation form (which can appear here) is
17603        // resolved by Collation::from_collation_name on the bare
17604        // identifier after the dot.
17605        let mut collation = Collation::Binary;
17606        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17607        // clause was written. The engine needs this to tell an explicit
17608        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17609        // clause at all: both resolve to `Collation::Binary`, but under the
17610        // MySQL dialect the latter takes the folding default collation.
17611        let mut collation_explicit = false;
17612        let mut collation_name: Option<alloc::string::String> = None;
17613        loop {
17614            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17615                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17616            {
17617                self.advance(); // CHARACTER
17618                self.advance(); // SET
17619                if matches!(
17620                    self.peek(),
17621                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17622                ) {
17623                    self.advance();
17624                }
17625                continue;
17626            }
17627            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17628                self.advance(); // COLLATE
17629                // Accept Ident / QuotedIdent / String AND the
17630                // keyword-tokenised `Default` (PG `pg_catalog.default`
17631                // and bare `DEFAULT` collation names — `default` is a
17632                // reserved word so the lexer hands back Token::Default
17633                // not Token::Ident).
17634                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17635                    match this.peek().clone() {
17636                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17637                            this.advance();
17638                            Some(s)
17639                        }
17640                        Token::Default => {
17641                            this.advance();
17642                            Some(alloc::string::String::from("default"))
17643                        }
17644                        _ => None,
17645                    }
17646                };
17647                let raw = if let Some(head) = read_collation_atom(self) {
17648                    // Schema-qualified PG form: `pg_catalog.default`.
17649                    if matches!(self.peek(), Token::Dot) {
17650                        self.advance();
17651                        let tail = read_collation_atom(self).unwrap_or_default();
17652                        alloc::format!("{head}.{tail}")
17653                    } else {
17654                        head
17655                    }
17656                } else {
17657                    alloc::string::String::new()
17658                };
17659                if !raw.is_empty() {
17660                    collation_explicit = true;
17661                    // v7.39 (round 676) — keep the name too. The enum below
17662                    // folds C / POSIX / en_US / default into one value, and
17663                    // `pg_attribute.attcollation` has to tell them apart.
17664                    // The schema qualifier goes: PG's `pg_catalog.default`
17665                    // and a bare `default` name the same collation.
17666                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17667                    // encoding suffix. Round 676 used `rsplit('.')` for
17668                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17669                    // PG writes `pg_catalog.default` (qualifier) and
17670                    // `en_US.utf8` (locale + encoding) with the same
17671                    // separator. Only `pg_catalog.` is a qualifier, and it
17672                    // is the only one PG's own dumps emit.
17673                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17674                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17675                    collation_name = Some(alloc::string::String::from(bare));
17676                    let parsed = Collation::from_collation_name(&raw);
17677                    // Last COLLATE clause wins, but `Binary` from a
17678                    // bare keyword like `default` should not
17679                    // silently downgrade a stronger one set earlier
17680                    // on the same column. v7.17 only ships one
17681                    // non-Binary variant so a simple OR is enough.
17682                    if parsed != Collation::Binary {
17683                        collation = parsed;
17684                    }
17685                }
17686                continue;
17687            }
17688            break;
17689        }
17690        // v7.10.10 — postfix `[]` widens the base type to its array
17691        // type. PG accepts `TYPE[]` after any base type and so does
17692        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17693        // all through; the old "only TEXT[]" note was stale).
17694        if matches!(self.peek(), Token::LBracket) {
17695            self.advance();
17696            if !matches!(self.peek(), Token::RBracket) {
17697                return Err(self.err(alloc::format!(
17698                    "TEXT[] takes no dimension; got {:?}",
17699                    self.peek()
17700                )));
17701            }
17702            self.advance();
17703            // v7.11.13 — widened to INT[] and BIGINT[] in addition
17704            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17705            // still error here.
17706            ty = match ty {
17707                ColumnTypeName::Text => ColumnTypeName::TextArray,
17708                ColumnTypeName::Int => ColumnTypeName::IntArray,
17709                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17710                // v7.40.0 — `oid[]`. Everything downstream of the
17711                // parser already handled `DataType::OidArray`; this
17712                // arm is the whole of what was missing.
17713                ColumnTypeName::Oid => ColumnTypeName::OidArray,
17714                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17715                // `[]` grammar. Wire OID 1187.
17716                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17717                // v7.37.5 γ — full PG array-of-scalar family.
17718                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17719                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17720                ColumnTypeName::Float => ColumnTypeName::FloatArray,
17721                // NUMERIC(p, s) loses its precision params at the
17722                // array level (matches PG: `NUMERIC[]` is untyped,
17723                // per-element precision flows through values).
17724                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17725                ColumnTypeName::Date => ColumnTypeName::DateArray,
17726                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17727                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17728                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17729                ColumnTypeName::Json => ColumnTypeName::JsonArray,
17730                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17731                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17732                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17733                // the array level (matches PG semantics where the
17734                // element precision is per-row, not column-wide).
17735                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17736                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17737                // v7.40.0 — TIME(p)[] / TIMETZ(p)[] drop the
17738                // precision the same way NUMERIC[] does.
17739                ColumnTypeName::Real => ColumnTypeName::RealArray,
17740                ColumnTypeName::Time => ColumnTypeName::TimeArray,
17741                ColumnTypeName::TimeTz => ColumnTypeName::TimeTzArray,
17742                ColumnTypeName::Inet => ColumnTypeName::InetArray,
17743                ColumnTypeName::Xml => ColumnTypeName::XmlArray,
17744                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17745                // follow-up.
17746                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17747                other => {
17748                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17749                }
17750            };
17751            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17752            // for INT/TEXT/BIGINT. Anything else is an error.
17753            if matches!(self.peek(), Token::LBracket) {
17754                self.advance();
17755                if !matches!(self.peek(), Token::RBracket) {
17756                    return Err(self.err(alloc::format!(
17757                        "TYPE[][] second dimension takes no size; got {:?}",
17758                        self.peek()
17759                    )));
17760                }
17761                self.advance();
17762                ty = match ty {
17763                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17764                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17765                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17766                    // v7.39 (read01 round 75) — bool[][].
17767                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17768                    other => {
17769                        return Err(self.err(alloc::format!(
17770                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17771                             TEXT[][] only; got {other:?}"
17772                        )));
17773                    }
17774                };
17775            }
17776        }
17777        Ok((
17778            ty,
17779            implied_auto_increment,
17780            implied_not_null,
17781            user_type_ref,
17782            collation,
17783            collation_explicit,
17784            collation_name,
17785            is_unsigned,
17786            inline_enum_variants,
17787            inline_set_variants,
17788            mysql_int_width,
17789            mysql_fsp,
17790            mysql_declared_timestamp,
17791            mysql_float_md,
17792        ))
17793    }
17794
17795    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17796        // v7.20 — PG reserves the table-constraint keywords, so a
17797        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17798        // malformed constraint clause (e.g. `UNIQUE a` missing its
17799        // parens), not a column named "unique". Since v7.17's
17800        // unknown-type leniency (`user_type_ref`) such a clause
17801        // would otherwise parse as a column with a user-defined
17802        // type — silently accepting invalid DDL. Quoted
17803        // identifiers ("unique" / `unique`) remain valid names.
17804        if let Token::Ident(s) = self.peek()
17805            && [
17806                "unique",
17807                "primary",
17808                "foreign",
17809                "constraint",
17810                "check",
17811                "references",
17812                "exclude",
17813            ]
17814            .iter()
17815            .any(|kw| s.eq_ignore_ascii_case(kw))
17816        {
17817            return Err(self.err(alloc::format!(
17818                "unexpected reserved keyword '{s}' at start of column definition \
17819                 (malformed table constraint?)"
17820            )));
17821        }
17822        let name_tok = self.pos;
17823        let name = self.expect_ident_like()?;
17824        // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17825        // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17826        // information_schema, and in SHOW CREATE (measured). SPG folded
17827        // an unquoted name, so a table restored from a dump reported
17828        // names the application had never written.
17829        //
17830        // The written form comes back from the source span, which only
17831        // the MySQL dialect keeps. The span runs to the START of the
17832        // next token, so a comment or unusual spacing between them
17833        // arrives with it — hence the check that what came back is the
17834        // same identifier. It is not decoration: without it,
17835        // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17836        // `MyCol /* c */`.
17837        let name = self
17838            .source_span(name_tok, name_tok)
17839            .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17840            .filter(|raw| raw.eq_ignore_ascii_case(&name))
17841            .map_or(name, alloc::string::String::from);
17842        let (
17843            ty,
17844            implied_auto_increment,
17845            implied_not_null,
17846            user_type_ref,
17847            collation,
17848            collation_explicit,
17849            collation_name,
17850            is_unsigned,
17851            inline_enum_variants,
17852            inline_set_variants,
17853            mysql_int_width,
17854            mysql_fsp,
17855            mysql_declared_timestamp,
17856            mysql_float_md,
17857        ) = self.parse_type_with_implied_flags()?;
17858        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17859        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17860        // each at most once.
17861        let mut default: Option<Expr> = None;
17862        let mut nullable = !implied_not_null;
17863        let mut nullability_seen = implied_not_null;
17864        let mut auto_increment = implied_auto_increment;
17865        let mut is_primary_key = false;
17866        let mut is_unique = false;
17867        let mut unique_nulls_not_distinct = false;
17868        let mut constraint_deferrable = false;
17869        let mut constraint_initially_deferred = false;
17870        let mut check: Option<Expr> = None;
17871        let mut on_update_runtime: Option<Expr> = None;
17872        let mut generated_stored_expr: Option<Box<Expr>> = None;
17873        let mut identity_always = false;
17874        loop {
17875            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17876            // not-null constraints by name and pg_dump emits them
17877            // inline: `id bigint CONSTRAINT contacts_id_not_null1
17878            // NOT NULL`. Accept and discard the name; whatever
17879            // constraint follows is parsed by the arms below.
17880            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17881                // v7.39 (round 308, V29) — a name on an inline
17882                // REFERENCES belongs to the FOREIGN KEY, and the caller
17883                // (`parse_column_def_with_fk`) is what builds it, so
17884                // leave the whole clause for it. Dropping the name here
17885                // is what made `CONSTRAINT fk_a REFERENCES …` come back
17886                // as the synthesised `c_pid_fkey` — which then could
17887                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17888                // `advance()` takes tokens by `mem::replace`, so there
17889                // is no rewinding once consumed.
17890                if matches!(
17891                    self.tokens.get(self.pos + 2),
17892                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17893                ) {
17894                    break;
17895                }
17896                self.advance();
17897                let _name = self.expect_ident_like()?;
17898                continue;
17899            }
17900            // v7.39 (round 379) — MySQL's SHORT generated-column form
17901            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17902            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17903            // below), but hand-written schemas and app migrations use this.
17904            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17905            // SPG computes-and-stores either way, like the long form.
17906            if matches!(self.peek(), Token::As) {
17907                self.advance();
17908                if !matches!(self.peek(), Token::LParen) {
17909                    return Err(self.err(alloc::format!(
17910                        "expected '(' after AS in a generated column, got {:?}",
17911                        self.peek()
17912                    )));
17913                }
17914                self.advance();
17915                let expr = self.parse_expr(0)?;
17916                if !matches!(self.peek(), Token::RParen) {
17917                    return Err(self.err(alloc::format!(
17918                        "expected ')' after AS (<expr>), got {:?}",
17919                        self.peek()
17920                    )));
17921                }
17922                self.advance();
17923                if matches!(self.peek(), Token::Ident(s)
17924                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17925                {
17926                    self.advance();
17927                }
17928                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17929                continue;
17930            }
17931            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17932            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17933            // the modern replacement for SERIAL in hand-written
17934            // schemas). Both flavours map onto the auto-increment
17935            // machinery — SPG's serial semantics ≈ BY DEFAULT;
17936            // ALWAYS's reject-explicit-values nuance is documented
17937            // leniency. Generated EXPRESSION columns
17938            // (`AS (expr) STORED`) are not supported: error loudly
17939            // instead of silently storing NULLs.
17940            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17941                self.advance();
17942                let mut saw_generated_always = false;
17943                match self.peek().clone() {
17944                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17945                        self.advance();
17946                        saw_generated_always = true;
17947                    }
17948                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17949                        self.advance();
17950                        if !matches!(self.peek(), Token::Default) {
17951                            return Err(self.err(alloc::format!(
17952                                "expected DEFAULT after GENERATED BY, got {:?}",
17953                                self.peek()
17954                            )));
17955                        }
17956                        self.advance();
17957                    }
17958                    other => {
17959                        return Err(self.err(alloc::format!(
17960                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
17961                        )));
17962                    }
17963                }
17964                if !matches!(self.peek(), Token::As) {
17965                    return Err(self.err(alloc::format!(
17966                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
17967                        self.peek()
17968                    )));
17969                }
17970                self.advance();
17971                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
17972                // ( <expr> ) STORED` stored computed-column. The
17973                // expression is captured for the engine to recompute
17974                // on every INSERT / UPDATE. v7.37.7 accepts the
17975                // STORED keyword only; PG also has VIRTUAL, which
17976                // v7.37.7 carves out (sentori only uses STORED).
17977                if matches!(self.peek(), Token::LParen) {
17978                    self.advance();
17979                    let expr = self.parse_expr(0)?;
17980                    if !matches!(self.peek(), Token::RParen) {
17981                        return Err(self.err(alloc::format!(
17982                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
17983                            self.peek()
17984                        )));
17985                    }
17986                    self.advance();
17987                    let stored = match self.peek() {
17988                        Token::Ident(s) | Token::QuotedIdent(s)
17989                            if s.eq_ignore_ascii_case("stored") =>
17990                        {
17991                            self.advance();
17992                            true
17993                        }
17994                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
17995                        // generated columns. SPG computes them on write and
17996                        // persists like STORED; the two are observably
17997                        // identical for query results (the value, recompute
17998                        // on base-column change, and NOT NULL enforcement all
17999                        // match), so a PG 18 schema/dump using VIRTUAL loads
18000                        // and behaves correctly. The compute-on-read storage
18001                        // saving is an invisible internal difference.
18002                        Token::Ident(s) | Token::QuotedIdent(s)
18003                            if s.eq_ignore_ascii_case("virtual") =>
18004                        {
18005                            self.advance();
18006                            false
18007                        }
18008                        other => {
18009                            return Err(self.err(alloc::format!(
18010                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
18011                                 got {other:?}"
18012                            )));
18013                        }
18014                    };
18015                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
18016                    generated_stored_expr = Some(Box::new(expr));
18017                    continue;
18018                }
18019                self.expect_keyword_ident("identity")?;
18020                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
18021                // consume the balanced parens and discard (SPG's
18022                // auto-increment is max+1-scan based).
18023                if matches!(self.peek(), Token::LParen) {
18024                    let mut depth = 0usize;
18025                    loop {
18026                        match self.advance() {
18027                            Token::LParen => depth += 1,
18028                            Token::RParen => {
18029                                depth -= 1;
18030                                if depth == 0 {
18031                                    break;
18032                                }
18033                            }
18034                            Token::Eof => {
18035                                return Err(self.err(
18036                                    "unterminated sequence-options parens after IDENTITY".into(),
18037                                ));
18038                            }
18039                            _ => {}
18040                        }
18041                    }
18042                }
18043                auto_increment = true;
18044                // v7.38 (read01) — remember the ALWAYS flavour so the engine
18045                // can reject explicit non-DEFAULT INSERT values (unless
18046                // OVERRIDING SYSTEM VALUE) the way PG does.
18047                identity_always = saw_generated_always;
18048                // PG identity columns are implicitly NOT NULL.
18049                nullable = false;
18050                continue;
18051            }
18052            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
18053            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
18054            // is accepted today. The "ON" token is an Ident
18055            // (not reserved) — peek before consuming.
18056            if matches!(self.peek(), Token::On)
18057                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
18058            {
18059                self.advance(); // ON
18060                self.advance(); // update
18061                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
18062                let next = self.peek().clone();
18063                match next {
18064                    Token::Ident(s) | Token::QuotedIdent(s)
18065                        if s.eq_ignore_ascii_case("current_timestamp") =>
18066                    {
18067                        self.advance();
18068                        // Optional `(N)` precision.
18069                        if matches!(self.peek(), Token::LParen) {
18070                            self.advance();
18071                            if !matches!(self.peek(), Token::Integer(_)) {
18072                                return Err(self.err(alloc::format!(
18073                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
18074                                    self.peek()
18075                                )));
18076                            }
18077                            self.advance();
18078                            if !matches!(self.peek(), Token::RParen) {
18079                                return Err(self.err(alloc::format!(
18080                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
18081                                    self.peek()
18082                                )));
18083                            }
18084                            self.advance();
18085                        }
18086                        on_update_runtime = Some(Expr::FunctionCall {
18087                            name: "now".into(),
18088                            args: Vec::new(),
18089                        });
18090                        continue;
18091                    }
18092                    other => {
18093                        return Err(self.err(alloc::format!(
18094                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
18095                        )));
18096                    }
18097                }
18098            }
18099            if matches!(self.peek(), Token::Default) {
18100                if default.is_some() {
18101                    return Err(self.err("DEFAULT specified twice".into()));
18102                }
18103                self.advance();
18104                default = Some(self.parse_expr(0)?);
18105                continue;
18106            }
18107            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
18108            // token with NOT NULL and sits EARLIER in the loop than the
18109            // deferrability arm, so without the lookahead it was reported as
18110            // "NOT NULL specified twice" (or "expected NULL after NOT").
18111            if matches!(self.peek(), Token::Not)
18112                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
18113            {
18114                // NOT DEFERRABLE — explicit immediate; nothing to carry.
18115                self.consume_optional_deferrable_clauses()?;
18116                continue;
18117            }
18118            if matches!(self.peek(), Token::Not) {
18119                if nullability_seen {
18120                    return Err(self.err("NOT NULL specified twice".into()));
18121                }
18122                self.advance();
18123                if !matches!(self.peek(), Token::Null) {
18124                    return Err(self.err(format!(
18125                        "expected NULL after NOT in column def, got {:?}",
18126                        self.peek()
18127                    )));
18128                }
18129                self.advance();
18130                nullable = false;
18131                nullability_seen = true;
18132                continue;
18133            }
18134            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
18135            // "this column is nullable" marker (the default in
18136            // standard SQL anyway). mysqldump emits it routinely
18137            // (`col TYPE NULL DEFAULT NULL` for nullable
18138            // timestamps etc). Accept + no-op.
18139            if matches!(self.peek(), Token::Null) {
18140                if nullability_seen && !nullable {
18141                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
18142                    // sentence, PG18-measured (the table name is the
18143                    // caller's; the column half is exact).
18144                    return Err(self.err(alloc::format!(
18145                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
18146                    )));
18147                }
18148                self.advance();
18149                nullable = true;
18150                nullability_seen = true;
18151                continue;
18152            }
18153            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
18154            // arrives as a bare Ident. Match either, case-insensitive.
18155            if let Token::Ident(s) = self.peek()
18156                && (s.eq_ignore_ascii_case("auto_increment")
18157                    || s.eq_ignore_ascii_case("autoincrement"))
18158            {
18159                if auto_increment {
18160                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
18161                }
18162                self.advance();
18163                auto_increment = true;
18164                continue;
18165            }
18166            // v7.9.13 — inline `PRIMARY KEY` column constraint
18167            // (mailrs F1). Implies `NOT NULL`. The engine creates
18168            // a BTree index for the PK column at CREATE TABLE time
18169            // so FK parent-side index lookups resolve.
18170            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
18171            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
18172            // spelling was a parse error, so a pg_dump carrying one stopped
18173            // mid-restore. The clauses are consumed by the same helper the FK
18174            // path has used since round 288 and recorded nowhere: SPG enforces
18175            // the constraint IMMEDIATELY either way, which fails earlier than
18176            // PG inside a transaction that violates-then-repairs — a refusal,
18177            // not a wrong answer. True deferral is the open remainder of F08.
18178            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
18179                || (matches!(self.peek(), Token::Not)
18180                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
18181            {
18182                // v7.39 (round 711) — CARRIED now (the storing half of
18183                // F08); round 621 only consumed.
18184                let (d, idef) = self.consume_deferrable_clauses_timed()?;
18185                constraint_deferrable |= d;
18186                constraint_initially_deferred |= idef;
18187                continue;
18188            }
18189            if let Token::Ident(s) = self.peek()
18190                && s.eq_ignore_ascii_case("primary")
18191            {
18192                if is_primary_key {
18193                    return Err(self.err("PRIMARY KEY specified twice".into()));
18194                }
18195                // Peek-ahead for the required `KEY` token.
18196                let next = self.tokens.get(self.pos + 1);
18197                let next_is_key = matches!(
18198                    next,
18199                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
18200                );
18201                if !next_is_key {
18202                    return Err(self.err(format!(
18203                        "expected KEY after PRIMARY in column def, got {:?}",
18204                        next
18205                    )));
18206                }
18207                self.advance(); // PRIMARY
18208                self.advance(); // KEY
18209                is_primary_key = true;
18210                if nullability_seen && nullable {
18211                    return Err(self.err(
18212                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
18213                    ));
18214                }
18215                nullable = false;
18216                nullability_seen = true;
18217                continue;
18218            }
18219            // v7.13.0 — inline `UNIQUE` column constraint
18220            // (mailrs round-5 G2). Fold into a single-column
18221            // table-level UNIQUE at CREATE TABLE post-process time.
18222            if let Token::Ident(s) = self.peek()
18223                && s.eq_ignore_ascii_case("unique")
18224            {
18225                if is_unique {
18226                    return Err(self.err("UNIQUE specified twice".into()));
18227                }
18228                self.advance();
18229                is_unique = true;
18230                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
18231                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
18232                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
18233                    let n1 = self.tokens.get(self.pos + 1);
18234                    let n2 = self.tokens.get(self.pos + 2);
18235                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
18236                        self.advance(); // NULLS
18237                        self.advance(); // NOT
18238                        self.advance(); // DISTINCT
18239                        unique_nulls_not_distinct = true;
18240                    } else if matches!(n1, Some(Token::Distinct)) {
18241                        self.advance(); // NULLS
18242                        self.advance(); // DISTINCT
18243                    }
18244                }
18245                continue;
18246            }
18247            // v7.13.0 — inline `CHECK (<expr>)` column constraint
18248            // (mailrs round-5 G3). PG semantics: column-level
18249            // CHECK is equivalent to a table-level CHECK. Multiple
18250            // inline CHECKs on the same column AND together.
18251            if let Token::Ident(s) = self.peek()
18252                && s.eq_ignore_ascii_case("check")
18253            {
18254                self.advance();
18255                if !matches!(self.peek(), Token::LParen) {
18256                    return Err(self.err(alloc::format!(
18257                        "expected '(' after CHECK in column def, got {:?}",
18258                        self.peek()
18259                    )));
18260                }
18261                self.advance();
18262                let pred = self.parse_expr(0)?;
18263                if !matches!(self.peek(), Token::RParen) {
18264                    return Err(self.err(alloc::format!(
18265                        "expected ')' to close CHECK predicate, got {:?}",
18266                        self.peek()
18267                    )));
18268                }
18269                self.advance();
18270                check = Some(match check.take() {
18271                    Some(prev) => Expr::Binary {
18272                        op: BinOp::And,
18273                        lhs: Box::new(prev),
18274                        rhs: Box::new(pred),
18275                    },
18276                    None => pred,
18277                });
18278                continue;
18279            }
18280            break;
18281        }
18282        Ok(ColumnDef {
18283            name,
18284            ty,
18285            nullable,
18286            default,
18287            auto_increment,
18288            is_primary_key,
18289            is_unique,
18290            unique_nulls_not_distinct,
18291            constraint_deferrable,
18292            constraint_initially_deferred,
18293            check,
18294            user_type_ref,
18295            on_update_runtime,
18296            collation,
18297            collation_explicit,
18298            collation_name,
18299            is_unsigned,
18300            inline_enum_variants,
18301            inline_set_variants,
18302            generated_stored_expr,
18303            identity_always,
18304            mysql_int_width,
18305            mysql_fsp,
18306            mysql_declared_timestamp,
18307            mysql_float_md,
18308        })
18309    }
18310
18311    /// `NUMERIC` may appear without parameters, with one (precision
18312    /// only, scale=0), or with both. Returns `(precision, scale)` with
18313    /// 0 = unspecified for the bare form.
18314    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
18315        if !matches!(self.peek(), Token::LParen) {
18316            // Bare `NUMERIC` — PG treats this as "unlimited precision";
18317            // we surface it as precision=0 to mean "unconstrained" so
18318            // the engine doesn't need a separate variant.
18319            return Ok((0, 0));
18320        }
18321        self.advance();
18322        // v7.39 (round 272) — PG's declared precision runs to 1000, and
18323        // it words the out-of-range case with the value it saw. SPG
18324        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
18325        // accepts failed to parse at all; values wider than i128 are
18326        // carried by the arbitrary-precision form.
18327        let precision = match self.advance() {
18328            Token::Integer(n) if (1..=1000).contains(&n) => {
18329                u16::try_from(n).expect("range-checked")
18330            }
18331            Token::Integer(n) => {
18332                return Err(ParseError {
18333                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
18334                    token_pos: self.consumed_pos(),
18335                });
18336            }
18337            other => {
18338                return Err(ParseError {
18339                    message: format!(
18340                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
18341                    ),
18342                    token_pos: self.consumed_pos(),
18343                });
18344            }
18345        };
18346        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
18347        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
18348        // then overflows). A negative scale rounds to tens / hundreds / …
18349        let scale = if matches!(self.peek(), Token::Comma) {
18350            self.advance();
18351            let neg = if matches!(self.peek(), Token::Minus) {
18352                self.advance();
18353                true
18354            } else {
18355                false
18356            };
18357            match self.advance() {
18358                Token::Integer(n) => {
18359                    let signed = if neg { -n } else { n };
18360                    if !(-1000..=1000).contains(&signed) {
18361                        return Err(ParseError {
18362                            message: format!(
18363                                "NUMERIC scale {signed} must be between -1000 and 1000"
18364                            ),
18365                            token_pos: self.consumed_pos(),
18366                        });
18367                    }
18368                    i16::try_from(signed).expect("range-checked")
18369                }
18370                other => {
18371                    return Err(ParseError {
18372                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
18373                        token_pos: self.consumed_pos(),
18374                    });
18375                }
18376            }
18377        } else {
18378            0
18379        };
18380        if !matches!(self.peek(), Token::RParen) {
18381            return Err(self.err(format!(
18382                "expected ')' to close NUMERIC params, got {:?}",
18383                self.peek()
18384            )));
18385        }
18386        self.advance();
18387        Ok((precision, scale))
18388    }
18389
18390    /// Parse `(N)` where `N` is a positive integer literal — used by the
18391    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
18392    /// for the error message.
18393    /// v6.0.1: parse the optional `USING <encoding>` clause that
18394    /// follows `VECTOR(N)` in a column definition. Missing clause
18395    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18396    /// ident → `ParseError` listing the encodings recognised today.
18397    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18398        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18399            return Ok(VecEncoding::F32);
18400        }
18401        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18402        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18403        // consume the token when the very next token is a known
18404        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18405        // `USING` for the caller — it's the rewrite-expression form.
18406        let n1 = self.tokens.get(self.pos + 1);
18407        let next_is_encoding = matches!(
18408            n1,
18409            Some(Token::Ident(s))
18410                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18411        );
18412        if !next_is_encoding {
18413            return Ok(VecEncoding::F32);
18414        }
18415        self.advance();
18416        let enc_ident = match self.advance() {
18417            Token::Ident(s) => s,
18418            other => {
18419                return Err(self.err(format!(
18420                    "expected vector encoding after USING, got {other:?}"
18421                )));
18422            }
18423        };
18424        match enc_ident.to_ascii_lowercase().as_str() {
18425            "sq8" => Ok(VecEncoding::Sq8),
18426            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18427            // binary16 per-element storage.
18428            "half" => Ok(VecEncoding::F16),
18429            other => Err(self.err(format!(
18430                "unknown vector encoding {other:?}; supported: SQ8, HALF"
18431            ))),
18432        }
18433    }
18434
18435    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18436    /// without consuming it. Returns `Some(N)` when the next
18437    /// tokens are `( <int> )`; None otherwise. Used by the
18438    /// TINYINT classifier to decide whether to map to Bool or
18439    /// SmallInt.
18440    fn peek_optional_paren_size_value(&self) -> Option<i64> {
18441        if !matches!(self.peek(), Token::LParen) {
18442            return None;
18443        }
18444        let next = self.tokens.get(self.pos + 1)?;
18445        let n = match next {
18446            Token::Integer(n) => *n,
18447            _ => return None,
18448        };
18449        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18450            return None;
18451        }
18452        Some(n)
18453    }
18454
18455    /// v7.14.0 — consume an optional MySQL display-width
18456    /// parenthesised number after an integer type, returning
18457    /// nothing. `TINYINT(1)` etc.
18458    /// v7.39 (round 360) — does the parenthesised group ahead contain a
18459    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18460    fn peek_paren_has_comma(&self) -> bool {
18461        let mut i = self.pos + 1;
18462        let mut depth = 1usize;
18463        while depth > 0 {
18464            match self.tokens.get(i) {
18465                Some(Token::LParen) => depth += 1,
18466                Some(Token::RParen) => depth -= 1,
18467                Some(Token::Comma) if depth == 1 => return true,
18468                None | Some(Token::Eof) => return false,
18469                _ => {}
18470            }
18471            i += 1;
18472        }
18473        false
18474    }
18475
18476    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18477    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18478    /// fractional-seconds precision that drives write truncation and render
18479    /// padding, where `consume_optional_paren_size` throws it away.
18480    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18481    fn take_optional_paren_size(&mut self) -> Option<u8> {
18482        let Some(Token::Integer(n)) = self
18483            .tokens
18484            .get(self.pos + 1)
18485            .filter(|_| matches!(self.peek(), Token::LParen))
18486            .cloned()
18487        else {
18488            self.consume_optional_paren_size();
18489            return None;
18490        };
18491        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18492            self.consume_optional_paren_size();
18493            return None;
18494        }
18495        self.consume_optional_paren_size();
18496        u8::try_from(n).ok()
18497    }
18498
18499    fn consume_optional_paren_size(&mut self) {
18500        if !matches!(self.peek(), Token::LParen) {
18501            return;
18502        }
18503        self.advance();
18504        // Skip until matching RParen (allow nested or any tokens).
18505        let mut depth = 1usize;
18506        while depth > 0 {
18507            match self.peek() {
18508                Token::LParen => depth += 1,
18509                Token::RParen => depth -= 1,
18510                Token::Eof => return,
18511                _ => {}
18512            }
18513            self.advance();
18514        }
18515    }
18516
18517    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18518        if !matches!(self.peek(), Token::LParen) {
18519            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18520        }
18521        self.advance();
18522        let n = match self.advance() {
18523            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18524                message: format!("{label} size too large: {n}"),
18525                token_pos: self.consumed_pos(),
18526            })?,
18527            other => {
18528                return Err(ParseError {
18529                    message: format!("expected positive integer {label} size, got {other:?}"),
18530                    token_pos: self.consumed_pos(),
18531                });
18532            }
18533        };
18534        if !matches!(self.peek(), Token::RParen) {
18535            return Err(self.err(format!(
18536                "expected ')' after {label} size, got {:?}",
18537                self.peek()
18538            )));
18539        }
18540        self.advance();
18541        Ok(n)
18542    }
18543
18544    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18545    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18546    /// key, like MySQL) whose action skips conflicting rows.
18547    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18548    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18549    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18550    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18551    /// common bulk-upsert spellings —
18552    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18553    ///     REPLACE INTO t SELECT …
18554    /// — were a parse error / a duplicate-key failure respectively.
18555    ///
18556    /// Precedence: an explicitly written clause beats a statement-level flag.
18557    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18558    /// implicit `REPLACE` and `IGNORE` lowerings.
18559    fn parse_insert_conflict_clause(
18560        &mut self,
18561        replace: bool,
18562        ignore: bool,
18563    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18564        if let Some(c) = self.parse_optional_on_duplicate_key()? {
18565            return Ok(Some(c));
18566        }
18567        if let Some(c) = self.parse_optional_on_conflict()? {
18568            return Ok(Some(c));
18569        }
18570        if replace {
18571            // REPLACE INTO = delete-then-insert, which PG spells as
18572            // `ON CONFLICT DO UPDATE SET` over every column; the engine
18573            // reads an empty assignment list as "take the incoming row".
18574            return Ok(Some(crate::ast::OnConflictClause {
18575                target_columns: Vec::new(),
18576                index_where: None,
18577                constraint_name: None,
18578                mysql_lowered: true,
18579                action: crate::ast::OnConflictAction::Update {
18580                    assignments: Vec::new(),
18581                    where_: None,
18582                },
18583            }));
18584        }
18585        if ignore {
18586            return Ok(Some(Self::insert_ignore_clause()));
18587        }
18588        Ok(None)
18589    }
18590
18591    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18592    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18593    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18594    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18595    fn parse_optional_on_duplicate_key(
18596        &mut self,
18597    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18598        if !(matches!(self.peek(), Token::On)
18599            && matches!(self.tokens.get(self.pos + 1),
18600                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18601        {
18602            return Ok(None);
18603        }
18604        self.advance(); // ON
18605        self.advance(); // DUPLICATE
18606        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18607            return Err(self.err(format!(
18608                "expected KEY after ON DUPLICATE, got {:?}",
18609                self.peek()
18610            )));
18611        }
18612        self.advance();
18613        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18614            return Err(self.err(format!(
18615                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18616                self.peek()
18617            )));
18618        }
18619        self.advance();
18620        let mut assignments: Vec<(String, Expr)> = Vec::new();
18621        loop {
18622            let col = self.expect_ident_like()?;
18623            if !matches!(self.peek(), Token::Eq) {
18624                return Err(self.err(format!(
18625                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18626                    self.peek()
18627                )));
18628            }
18629            self.advance();
18630            let mut expr = self.parse_expr(0)?;
18631            Self::rewrite_mysql_values_refs(&mut expr);
18632            assignments.push((col, expr));
18633            if matches!(self.peek(), Token::Comma) {
18634                self.advance();
18635                continue;
18636            }
18637            break;
18638        }
18639        Ok(Some(crate::ast::OnConflictClause {
18640            target_columns: Vec::new(),
18641            index_where: None,
18642            constraint_name: None,
18643            mysql_lowered: true,
18644            action: crate::ast::OnConflictAction::Update {
18645                assignments,
18646                where_: None,
18647            },
18648        }))
18649    }
18650
18651    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18652        crate::ast::OnConflictClause {
18653            target_columns: Vec::new(),
18654            index_where: None,
18655            constraint_name: None,
18656            mysql_lowered: true,
18657            action: crate::ast::OnConflictAction::Nothing,
18658        }
18659    }
18660
18661    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18662        debug_assert!(
18663            matches!(self.peek(), Token::Insert)
18664                || (replace
18665                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18666        );
18667        self.advance();
18668        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18669        // would raise a duplicate-key error instead of failing the statement,
18670        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18671        // plain ident to the lexer; only the MySQL dialect accepts it here.
18672        let ignore = self.mysql_dialect
18673            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18674        if ignore {
18675            self.advance();
18676        }
18677        if !matches!(self.peek(), Token::Into) {
18678            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18679        }
18680        self.advance();
18681        let table = self.expect_ident_like()?;
18682        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18683        // grammar requires the AS keyword here (a bare identifier would be
18684        // ambiguous with a column list). The alias is what the ON CONFLICT
18685        // DO UPDATE expressions refer to the target row by.
18686        let alias = if matches!(self.peek(), Token::As) {
18687            self.advance();
18688            Some(self.expect_ident_like()?)
18689        } else {
18690            None
18691        };
18692        // v7.39 (round 428) — MySQL's SET-form INSERT:
18693        //     INSERT INTO t SET a = 1, b = 'x'
18694        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18695        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18696        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18697        // measured). So it lowers to the column list + one VALUES row and
18698        // rejoins the ordinary path, which already handles every one of
18699        // those. PG has no such spelling, hence the dialect gate.
18700        if self.mysql_dialect
18701            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18702        {
18703            self.advance(); // SET
18704            let mut names = Vec::new();
18705            let mut values = Vec::new();
18706            loop {
18707                names.push(self.expect_ident_like()?);
18708                if !matches!(self.peek(), Token::Eq) {
18709                    return Err(self.err(alloc::format!(
18710                        "expected '=' in INSERT … SET, got {:?}",
18711                        self.peek()
18712                    )));
18713                }
18714                self.advance();
18715                // `SET a = DEFAULT` rides the same `__column_default` marker
18716                // the VALUES-row and UPDATE-SET paths use; the INSERT
18717                // executor resolves it against the target column.
18718                if matches!(self.peek(), Token::Default) {
18719                    self.advance();
18720                    values.push(Expr::FunctionCall {
18721                        name: "__column_default".to_string(),
18722                        args: Vec::new(),
18723                    });
18724                } else {
18725                    values.push(self.parse_expr(0)?);
18726                }
18727                if matches!(self.peek(), Token::Comma) {
18728                    self.advance();
18729                    continue;
18730                }
18731                break;
18732            }
18733            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18734            let returning = self.parse_optional_returning()?;
18735            return Ok(Statement::Insert(InsertStatement {
18736                ctes: Vec::new(),
18737                table,
18738                alias,
18739                columns: Some(names),
18740                rows: alloc::vec![values],
18741                select_source: None,
18742                // MySQL's SET form has no `OVERRIDING …` clause (that is
18743                // PG's identity-column spelling).
18744                overriding: Overriding::None,
18745                mysql_ignore: ignore,
18746                on_conflict,
18747                returning,
18748            }));
18749        }
18750        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18751        // v7.39 (round 151) — a SELECT or WITH right after the paren is
18752        // a parenthesized query source instead (PG select_with_parens:
18753        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18754        // both keywords are reserved in PG, so no column list can start
18755        // with them.
18756        let columns = if matches!(self.peek(), Token::LParen) {
18757            self.advance();
18758            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18759                let select_stmt = if self.peek_is_with_kw() {
18760                    self.advance();
18761                    self.parse_nested_with_select()?
18762                } else {
18763                    match self.parse_select_stmt()? {
18764                        Statement::Select(s) => s,
18765                        other => {
18766                            return Err(self.err(alloc::format!(
18767                                "expected SELECT in parenthesized INSERT source, got {other:?}"
18768                            )));
18769                        }
18770                    }
18771                };
18772                if !matches!(self.peek(), Token::RParen) {
18773                    return Err(self.err(format!(
18774                        "expected ')' after parenthesized INSERT source, got {:?}",
18775                        self.peek()
18776                    )));
18777                }
18778                self.advance();
18779                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18780                let returning = self.parse_optional_returning()?;
18781                return Ok(Statement::Insert(InsertStatement {
18782                    ctes: Vec::new(),
18783                    table,
18784                    alias: alias.clone(),
18785                    columns: None,
18786                    rows: Vec::new(),
18787                    select_source: Some(Box::new(select_stmt)),
18788                    on_conflict,
18789                    returning,
18790                    overriding: Overriding::None,
18791                    mysql_ignore: ignore,
18792                }));
18793            }
18794            let mut names = Vec::new();
18795            loop {
18796                names.push(self.expect_ident_like()?);
18797                match self.peek() {
18798                    Token::Comma => {
18799                        self.advance();
18800                    }
18801                    Token::RParen => {
18802                        self.advance();
18803                        break;
18804                    }
18805                    other => {
18806                        return Err(self.err(format!(
18807                            "expected ',' or ')' in INSERT column list, got {other:?}"
18808                        )));
18809                    }
18810                }
18811            }
18812            Some(names)
18813        } else {
18814            None
18815        };
18816        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18817        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18818        // is captured on the statement so the engine can apply PG's
18819        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18820        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18821        {
18822            self.advance();
18823            let which = self.expect_ident_like()?;
18824            let ov = if which.eq_ignore_ascii_case("system") {
18825                Overriding::System
18826            } else if which.eq_ignore_ascii_case("user") {
18827                Overriding::User
18828            } else {
18829                return Err(self.err(format!(
18830                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18831                )));
18832            };
18833            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18834                return Err(self.err(format!(
18835                    "expected VALUE after OVERRIDING {}, got {:?}",
18836                    which.to_ascii_uppercase(),
18837                    self.peek()
18838                )));
18839            }
18840            self.advance();
18841            ov
18842        } else {
18843            Overriding::None
18844        };
18845        // `INSERT INTO t DEFAULT VALUES` — a single row made
18846        // entirely of column defaults. Lower to the permuted
18847        // column-list path with an empty list: every schema column
18848        // is unmapped, so the engine fills each from its default
18849        // (serials advance, plain defaults evaluate, the rest NULL).
18850        if matches!(self.peek(), Token::Default) {
18851            self.advance();
18852            if !matches!(self.peek(), Token::Values) {
18853                return Err(self.err(format!(
18854                    "expected VALUES after DEFAULT in INSERT, got {:?}",
18855                    self.peek()
18856                )));
18857            }
18858            self.advance();
18859            if columns.is_some() {
18860                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18861            }
18862            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18863            let returning = self.parse_optional_returning()?;
18864            return Ok(Statement::Insert(InsertStatement {
18865                ctes: Vec::new(),
18866                table,
18867                alias: alias.clone(),
18868                columns: Some(Vec::new()),
18869                rows: alloc::vec![Vec::new()],
18870                select_source: None,
18871                on_conflict,
18872                returning,
18873                overriding,
18874                mysql_ignore: ignore,
18875            }));
18876        }
18877        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18878        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18879        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18880        // SELECT …`) heads the SOURCE select, as in PG (the statement's
18881        // own WITH comes before INSERT).
18882        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18883            let select_stmt = if self.peek_is_with_kw() {
18884                self.advance();
18885                self.parse_nested_with_select()?
18886            } else {
18887                match self.parse_select_stmt()? {
18888                    Statement::Select(s) => s,
18889                    other => {
18890                        return Err(self.err(alloc::format!(
18891                            "expected SELECT after INSERT INTO ... target, got {other:?}"
18892                        )));
18893                    }
18894                }
18895            };
18896            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18897            let returning = self.parse_optional_returning()?;
18898            return Ok(Statement::Insert(InsertStatement {
18899                ctes: Vec::new(),
18900                table,
18901                alias: alias.clone(),
18902                columns,
18903                rows: Vec::new(),
18904                select_source: Some(Box::new(select_stmt)),
18905                on_conflict,
18906                returning,
18907                overriding,
18908                mysql_ignore: ignore,
18909            }));
18910        }
18911        if !matches!(self.peek(), Token::Values) {
18912            return Err(self.err(format!(
18913                "expected VALUES or SELECT after table name, got {:?}",
18914                self.peek()
18915            )));
18916        }
18917        self.advance();
18918        if !matches!(self.peek(), Token::LParen) {
18919            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18920        }
18921        let mut rows = Vec::new();
18922        loop {
18923            // Each iteration consumes one `(expr, expr, …)` tuple.
18924            if !matches!(self.peek(), Token::LParen) {
18925                return Err(self.err(format!(
18926                    "expected '(' for next VALUES tuple, got {:?}",
18927                    self.peek()
18928                )));
18929            }
18930            self.advance();
18931            let mut tuple = Vec::new();
18932            loop {
18933                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18934                // the column's declared default for that slot. Rides out as the
18935                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18936                // path uses; the INSERT executor resolves it per target column.
18937                if matches!(self.peek(), Token::Default) {
18938                    self.advance();
18939                    tuple.push(Expr::FunctionCall {
18940                        name: "__column_default".to_string(),
18941                        args: Vec::new(),
18942                    });
18943                } else {
18944                    tuple.push(self.parse_expr(0)?);
18945                }
18946                match self.peek() {
18947                    Token::Comma => {
18948                        self.advance();
18949                    }
18950                    Token::RParen => {
18951                        self.advance();
18952                        break;
18953                    }
18954                    other => {
18955                        return Err(self.err(format!(
18956                            "expected ',' or ')' in VALUES tuple, got {other:?}"
18957                        )));
18958                    }
18959                }
18960            }
18961            if tuple.is_empty() {
18962                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
18963            }
18964            rows.push(tuple);
18965            // Continue with comma-separated tuples.
18966            if matches!(self.peek(), Token::Comma) {
18967                self.advance();
18968            } else {
18969                break;
18970            }
18971        }
18972        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
18973        // to ON CONFLICT DO UPDATE with an empty conflict target
18974        // (the engine picks the table's first unique index, which
18975        // matches MySQL's any-unique-key behaviour for the common
18976        // single-key case). `VALUES(col)` in the assignments is
18977        // MySQL's spelling of EXCLUDED.col.
18978        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18979        let returning = self.parse_optional_returning()?;
18980        Ok(Statement::Insert(InsertStatement {
18981            ctes: Vec::new(),
18982            table,
18983            alias,
18984            columns,
18985            rows,
18986            select_source: None,
18987            on_conflict,
18988            returning,
18989            overriding,
18990            mysql_ignore: ignore,
18991        }))
18992    }
18993
18994    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
18995    /// the incoming row's value — exactly PG's EXCLUDED.col.
18996    fn rewrite_mysql_values_refs(e: &mut Expr) {
18997        match e {
18998            Expr::FunctionCall { name, args }
18999                if name.eq_ignore_ascii_case("values")
19000                    && args.len() == 1
19001                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
19002            {
19003                let Expr::Column(c) = &args[0] else {
19004                    unreachable!("guarded above");
19005                };
19006                *e = Expr::Column(crate::ast::ColumnName {
19007                    qualifier: Some("EXCLUDED".to_string()),
19008                    name: c.name.clone(),
19009                });
19010            }
19011            Expr::FunctionCall { args, .. } => {
19012                for a in args {
19013                    Self::rewrite_mysql_values_refs(a);
19014                }
19015            }
19016            Expr::Binary { lhs, rhs, .. } => {
19017                Self::rewrite_mysql_values_refs(lhs);
19018                Self::rewrite_mysql_values_refs(rhs);
19019            }
19020            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19021                Self::rewrite_mysql_values_refs(expr);
19022            }
19023            Expr::Case {
19024                operand,
19025                branches,
19026                else_branch,
19027            } => {
19028                if let Some(op) = operand {
19029                    Self::rewrite_mysql_values_refs(op);
19030                }
19031                for (w, t) in branches {
19032                    Self::rewrite_mysql_values_refs(w);
19033                    Self::rewrite_mysql_values_refs(t);
19034                }
19035                if let Some(el) = else_branch {
19036                    Self::rewrite_mysql_values_refs(el);
19037                }
19038            }
19039            _ => {}
19040        }
19041    }
19042
19043    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
19044    /// clause sitting between the INSERT body and the trailing
19045    /// RETURNING. All keywords come in as bare idents; `ON` is
19046    /// a reserved Token though.
19047    fn parse_optional_on_conflict(
19048        &mut self,
19049    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
19050        if !matches!(self.peek(), Token::On) {
19051            return Ok(None);
19052        }
19053        // Peek further: we want exactly "ON CONFLICT ...". If the
19054        // next ident isn't "conflict", let some other parser handle.
19055        let next_is_conflict = matches!(
19056            self.tokens.get(self.pos + 1),
19057            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
19058        );
19059        if !next_is_conflict {
19060            return Ok(None);
19061        }
19062        self.advance(); // ON
19063        self.advance(); // CONFLICT
19064        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
19065        // the constraint instead of listing columns (the pg_dump
19066        // form); the engine resolves it.
19067        let mut constraint_name: Option<String> = None;
19068        if matches!(self.peek(), Token::On) {
19069            self.advance(); // ON
19070            match self.advance() {
19071                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
19072                }
19073                other => {
19074                    return Err(self.err(alloc::format!(
19075                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
19076                    )));
19077                }
19078            }
19079            constraint_name = Some(self.expect_ident_like()?);
19080        }
19081        // Optional `(col [, col]*)` target list.
19082        let mut target_columns: Vec<String> = Vec::new();
19083        if matches!(self.peek(), Token::LParen) {
19084            self.advance();
19085            loop {
19086                target_columns.push(self.expect_ident_like()?);
19087                match self.peek() {
19088                    Token::Comma => {
19089                        self.advance();
19090                    }
19091                    Token::RParen => {
19092                        self.advance();
19093                        break;
19094                    }
19095                    other => {
19096                        return Err(self.err(alloc::format!(
19097                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
19098                        )));
19099                    }
19100                }
19101            }
19102        }
19103        // v7.39 (round 240) — optional index predicate after the target
19104        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
19105        // PARTIAL unique index; SPG's arbiters are full indexes, which
19106        // satisfy any predicate, so it is parsed and carried but not
19107        // consulted (recorded residual: partial-unique-index arbiters).
19108        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
19109            self.advance();
19110            Some(self.parse_expr(0)?)
19111        } else {
19112            None
19113        };
19114        // Required `DO`.
19115        match self.advance() {
19116            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
19117            other => {
19118                return Err(self.err(alloc::format!(
19119                    "expected DO after ON CONFLICT [(…)], got {other:?}"
19120                )));
19121            }
19122        }
19123        // Action: NOTHING | UPDATE SET …
19124        let action = match self.advance() {
19125            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
19126                crate::ast::OnConflictAction::Nothing
19127            }
19128            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
19129                self.parse_on_conflict_update_action()?
19130            }
19131            other => {
19132                return Err(self.err(alloc::format!(
19133                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
19134                )));
19135            }
19136        };
19137        Ok(Some(crate::ast::OnConflictClause {
19138            target_columns,
19139            index_where,
19140            constraint_name,
19141            mysql_lowered: false,
19142            action,
19143        }))
19144    }
19145
19146    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
19147    /// `SET col = expr [, …] [WHERE cond]`. Caller already
19148    /// consumed `UPDATE`.
19149    fn parse_on_conflict_update_action(
19150        &mut self,
19151    ) -> Result<crate::ast::OnConflictAction, ParseError> {
19152        // `SET`
19153        match self.advance() {
19154            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
19155            other => {
19156                return Err(self.err(alloc::format!(
19157                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
19158                )));
19159            }
19160        }
19161        let mut assignments: Vec<(String, Expr)> = Vec::new();
19162        loop {
19163            let col = self.expect_ident_like()?;
19164            if !matches!(self.peek(), Token::Eq) {
19165                return Err(self.err(alloc::format!(
19166                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
19167                    self.peek()
19168                )));
19169            }
19170            self.advance();
19171            let value = self.parse_expr(0)?;
19172            assignments.push((col, value));
19173            if matches!(self.peek(), Token::Comma) {
19174                self.advance();
19175                continue;
19176            }
19177            break;
19178        }
19179        let where_ = if matches!(self.peek(), Token::Where) {
19180            self.advance();
19181            Some(self.parse_expr(0)?)
19182        } else {
19183            None
19184        };
19185        Ok(crate::ast::OnConflictAction::Update {
19186            assignments,
19187            where_,
19188        })
19189    }
19190
19191    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
19192        let mut items = Vec::new();
19193        // v7.39 (round 341, V66) — PG's target list may be EMPTY
19194        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
19195        // answers one zero-column row per row of t, and a bare `SELECT`
19196        // answers a single zero-column row. SPG required at least one
19197        // item, so both were syntax errors. Recognised by the token that
19198        // follows — nothing that can start an expression appears here.
19199        if self.select_list_is_empty_here() {
19200            return Ok(items);
19201        }
19202        loop {
19203            items.push(self.parse_select_item()?);
19204            if matches!(self.peek(), Token::Comma) {
19205                self.advance();
19206            } else {
19207                break;
19208            }
19209        }
19210        Ok(items)
19211    }
19212
19213    /// Is the target list empty at this point — i.e. does the next token
19214    /// end the SELECT's item list rather than start an item?
19215    fn select_list_is_empty_here(&self) -> bool {
19216        match self.peek() {
19217            Token::From
19218            | Token::Where
19219            | Token::Group
19220            | Token::Having
19221            | Token::Order
19222            | Token::Limit
19223            | Token::Offset
19224            | Token::Semicolon
19225            | Token::RParen
19226            | Token::Union
19227            | Token::Except
19228            | Token::Eof => true,
19229            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
19230            // with unreserved keywords, so they arrive as plain idents.
19231            Token::Ident(s) => {
19232                s.eq_ignore_ascii_case("fetch")
19233                    || s.eq_ignore_ascii_case("window")
19234                    || s.eq_ignore_ascii_case("intersect")
19235            }
19236            _ => false,
19237        }
19238    }
19239
19240    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
19241        if matches!(self.peek(), Token::Star) {
19242            self.advance();
19243            return Ok(SelectItem::Wildcard);
19244        }
19245        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
19246        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
19247        // choke on the `*` ("expected identifier, got Star"). The lookahead is
19248        // `<ident> . *` with nothing binding tighter.
19249        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
19250            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19251                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
19252            {
19253                self.advance(); // qualifier
19254                self.advance(); // .
19255                self.advance(); // *
19256                return Ok(SelectItem::QualifiedWildcard(q));
19257            }
19258        }
19259        let start_tok = self.pos;
19260        let expr = self.parse_expr(0)?;
19261        let end_tok = self.consumed_pos();
19262        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
19263        // multi-column function returns into columns. Marked here and lowered in
19264        // `parse_bare_select`, where the FROM clause is in hand.
19265        if matches!(self.peek(), Token::Dot)
19266            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
19267        {
19268            self.advance(); // .
19269            self.advance(); // *
19270            return Ok(SelectItem::Expr {
19271                expr: Expr::FunctionCall {
19272                    name: "__record_expand".to_string(),
19273                    args: alloc::vec![expr],
19274                },
19275                alias: None,
19276            });
19277        }
19278        // v7.39.2 — MySQL lets a STRING name a projection item, with or
19279        // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
19280        // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
19281        // `syntax error at or near "'x'"` to all of them.
19282        //
19283        // Only here, not in `parse_optional_alias`: that one also names
19284        // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
19285        // and `FROM t AS 'ta'` are both syntax errors, measured. And only
19286        // after the lexer's own rule has joined adjacent literals, or
19287        // `SELECT 'a' 'b'` would read as a literal aliased `b` where
19288        // MySQL answers the concatenation `ab`.
19289        if self.mysql_dialect {
19290            let at_as = matches!(self.peek(), Token::As)
19291                && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
19292            if at_as {
19293                self.advance();
19294            }
19295            if let Token::String(name) = self.peek().clone() {
19296                self.advance();
19297                return Ok(SelectItem::Expr {
19298                    expr,
19299                    alias: Some(name),
19300                });
19301            }
19302        }
19303        let alias = match self.parse_optional_alias()? {
19304            Some(a) => Some(a),
19305            None => self.mysql_item_label(&expr, start_tok, end_tok),
19306        };
19307        Ok(SelectItem::Expr { expr, alias })
19308    }
19309
19310    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
19311    /// carries no `AS`, filled in here so every downstream path reports it
19312    /// without knowing the rule. `None` leaves the item un-aliased, which is
19313    /// what a PG session always gets.
19314    ///
19315    /// Measured against MariaDB 11, three rules and no more:
19316    ///
19317    /// | item             | label      | why                          |
19318    /// |------------------|------------|------------------------------|
19319    /// | `lbl.a`          | `a`        | a column reports its name    |
19320    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
19321    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
19322    ///
19323    /// The third is why this lives in the parser at all: the label is the
19324    /// text the client WROTE, down to the spacing, so it cannot be printed
19325    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
19326    ///
19327    /// Comments survive, and that is right: through a `mariadb` CLI both
19328    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
19329    /// CLIENT stripping the comment before it sends. Asked over the raw
19330    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
19331    /// produces.
19332    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
19333        if !self.mysql_dialect {
19334            return None;
19335        }
19336        match expr {
19337            // A column already reports its own name downstream; naming it
19338            // again here would only re-state the qualifier the label drops.
19339            Expr::Column(_) => None,
19340            // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
19341            // `ab`, and MySQL 9.7.2 names the column `a`: the label is
19342            // the first segment as written, not the joined value
19343            // (measured). The lexer logs where it joined them.
19344            Expr::Literal(Literal::String(v)) => Some(
19345                self.merged_first_len(start_tok)
19346                    .and_then(|n| v.get(..n))
19347                    .map_or_else(|| v.clone(), String::from),
19348            ),
19349            _ => self.source_span(start_tok, end_tok).map(str::to_string),
19350        }
19351    }
19352
19353    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
19354    /// consumed VALUES keyword. Each row lowers to a constant SELECT
19355    /// with PG's default column1..columnN names; subsequent rows
19356    /// chain as UNION ALL peers. Shared by the FROM-position
19357    /// `( VALUES … )` arm and the top-level bare VALUES statement.
19358    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
19359        let mut row_selects: Vec<SelectStatement> = Vec::new();
19360        loop {
19361            if !matches!(self.peek(), Token::LParen) {
19362                return Err(self.err(alloc::format!(
19363                    "expected '(' to start a VALUES row, got {:?}",
19364                    self.peek()
19365                )));
19366            }
19367            self.advance(); // (
19368            let mut items: Vec<SelectItem> = Vec::new();
19369            loop {
19370                let expr = self.parse_expr(0)?;
19371                items.push(SelectItem::Expr {
19372                    expr,
19373                    alias: Some(alloc::format!("column{}", items.len() + 1)),
19374                });
19375                match self.peek() {
19376                    Token::Comma => {
19377                        self.advance();
19378                    }
19379                    Token::RParen => break,
19380                    other => {
19381                        return Err(self.err(alloc::format!(
19382                            "expected ',' or ')' in VALUES row, got {other:?}"
19383                        )));
19384                    }
19385                }
19386            }
19387            self.advance(); // )
19388            row_selects.push(SelectStatement {
19389                locking: None,
19390                ctes: Vec::new(),
19391                distinct: false,
19392                distinct_on: Vec::new(),
19393                items,
19394                from: None,
19395                where_: None,
19396                group_by: None,
19397                group_by_all: false,
19398                having: None,
19399                unions: Vec::new(),
19400                order_by: Vec::new(),
19401                limit: None,
19402                offset: None,
19403                limit_with_ties: false,
19404                window_check_exprs: Vec::new(),
19405            });
19406            if matches!(self.peek(), Token::Comma) {
19407                self.advance();
19408                continue;
19409            }
19410            break;
19411        }
19412        let mut head = row_selects.remove(0);
19413        head.unions = row_selects
19414            .into_iter()
19415            .map(|s| (UnionKind::All, s))
19416            .collect();
19417        Ok(head)
19418    }
19419
19420    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19421        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19422        // children. It was read as a table NAMED `only`, so the query
19423        // failed on `relation "only" does not exist`.
19424        //
19425        // v7.39 (round 644) — and it is no longer a no-op. Round 621
19426        // absorbed the keyword, reasoning that SPG's children are
19427        // separate relations a plain scan does not descend into, so ONLY
19428        // already described the scan. That stopped being true when a
19429        // partition parent started unioning its children: measured,
19430        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19431        // where PG answers 0. The flag is carried now.
19432        let mut only = false;
19433        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19434            && matches!(
19435                self.tokens.get(self.pos + 1),
19436                Some(Token::Ident(_) | Token::QuotedIdent(_))
19437            )
19438        {
19439            only = true;
19440            self.advance();
19441        }
19442        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19443        // for these SRFs the keyword is noise at parse time: the
19444        // join executor already substitutes outer-column references
19445        // into unnest_expr / generate_series_args per outer row
19446        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19447        // licences the correlation even without the keyword. Absorb
19448        // it and fall through to the SRF arms below.
19449        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19450        // just the four builtin SRFs: a user set-returning function on a JOIN's
19451        // right side is the whole point of LATERAL. The keyword stays noise at
19452        // parse time — the join executor substitutes the outer row into the
19453        // call's arguments per outer row.
19454        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19455            && matches!(
19456                self.tokens.get(self.pos + 1),
19457                // The json_each family has its OWN `LATERAL …` arm below, which
19458                // needs to see the keyword — absorbing it here would send those
19459                // calls down the generic table-function channel instead.
19460                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19461            )
19462            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19463        {
19464            self.advance(); // LATERAL
19465        }
19466        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19467        // set-returning function whose argument may reference a
19468        // preceding FROM item. We rewrite this to
19469        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19470        // AS __srf__) AS <alias>` so the existing LATERAL subquery
19471        // executor handles per-outer-row evaluation and the
19472        // SRF-primary jsonb_each_text path handles the inner
19473        // materialisation. Sentori 0067 backfill is the dogfood
19474        // shape.
19475        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19476            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19477            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19478        {
19479            self.advance(); // LATERAL
19480            let each_fn = match self.peek() {
19481                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19482                _ => unreachable!(),
19483            };
19484            self.advance(); // jsonb_each[_text] / json_each[_text]
19485            self.advance(); // (
19486            let arg = self.parse_expr(0)?;
19487            if !matches!(self.peek(), Token::RParen) {
19488                return Err(self.err(alloc::format!(
19489                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19490                    self.peek()
19491                )));
19492            }
19493            self.advance();
19494            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19495            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19496            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19497            //               FROM jsonb_each_text(<arg>) AS __srf__
19498            // PG's `AS kv(key, value)` column-alias list maps
19499            // positions to names; default to (key, value) when
19500            // omitted (matching the SRF's natural column names).
19501            let srf_alias = "__srf__".to_string();
19502            let key_alias = column_aliases
19503                .first()
19504                .cloned()
19505                .unwrap_or_else(|| "key".to_string());
19506            let value_alias = column_aliases
19507                .get(1)
19508                .cloned()
19509                .unwrap_or_else(|| "value".to_string());
19510            let inner_select = crate::ast::SelectStatement {
19511                locking: None,
19512                ctes: Vec::new(),
19513                distinct: false,
19514                distinct_on: Vec::new(),
19515                items: alloc::vec![
19516                    crate::ast::SelectItem::Expr {
19517                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19518                            qualifier: Some(srf_alias.clone()),
19519                            name: "key".to_string(),
19520                        }),
19521                        alias: Some(key_alias),
19522                    },
19523                    crate::ast::SelectItem::Expr {
19524                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19525                            qualifier: Some(srf_alias.clone()),
19526                            name: "value".to_string(),
19527                        }),
19528                        alias: Some(value_alias),
19529                    },
19530                ],
19531                from: Some(crate::ast::FromClause {
19532                    primary: TableRef {
19533                        name: srf_alias.clone(),
19534                        alias: Some(srf_alias.clone()),
19535                        only: false,
19536                        as_of_segment: None,
19537                        unnest_expr: None,
19538                        unnest_column_aliases: Vec::new(),
19539                        with_ordinality: false,
19540                        generate_series_args: None,
19541                        lateral_subquery: None,
19542                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19543                        table_fn_call: None,
19544                        rows_from: None,
19545                        json_table: None,
19546                        scalar_fn_item: false,
19547                    },
19548                    joins: Vec::new(),
19549                }),
19550                where_: None,
19551                group_by: None,
19552                group_by_all: false,
19553                having: None,
19554                unions: Vec::new(),
19555                order_by: Vec::new(),
19556                limit: None,
19557                offset: None,
19558                limit_with_ties: false,
19559                window_check_exprs: Vec::new(),
19560            };
19561            return Ok(TableRef {
19562                name: alias.clone(),
19563                alias: Some(alias),
19564                only: false,
19565                as_of_segment: None,
19566                unnest_expr: None,
19567                unnest_column_aliases: Vec::new(),
19568                with_ordinality: false,
19569                generate_series_args: None,
19570                lateral_subquery: Some(Box::new(inner_select)),
19571                jsonb_each_text_arg: None,
19572                table_fn_call: None,
19573                rows_from: None,
19574                json_table: None,
19575                scalar_fn_item: false,
19576            });
19577        }
19578        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19579        // without an explicit `LATERAL` keyword is the same shape
19580        // PG accepts (SRF naturally licences lateral correlation).
19581        // We mirror the LATERAL rewrite when the argument syntactic-
19582        // ally references an outer column (Column { qualifier:
19583        // Some(_), … }). For simplicity we apply the rewrite
19584        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19585        // in the FROM-list — caller-side join parsing positions
19586        // this peek correctly.
19587        // (Implementation note: detection lives below; the LATERAL
19588        // branch above already covers the explicit form; the bare
19589        // form falls through to the plain SRF arm and the engine
19590        // treats it as a constant-arg SRF if no outer reference is
19591        // present.)
19592        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19593        // table. Detect at the head so it claims precedence over
19594        // every other table-ref shape (unnest / generate_series /
19595        // bare ident); the lateral subquery itself follows the
19596        // regular SELECT grammar.
19597        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19598        // t(cols)`. Each row lowers to a constant SELECT with PG's
19599        // default column1..columnN names; subsequent rows chain as
19600        // UNION ALL peers. The result rides the derived-table
19601        // lateral_subquery channel — zero executor work.
19602        if matches!(self.peek(), Token::LParen)
19603            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19604        {
19605            self.advance(); // (
19606            self.advance(); // VALUES
19607            let head = self.parse_values_rows_body()?;
19608            if !matches!(self.peek(), Token::RParen) {
19609                return Err(self.err(alloc::format!(
19610                    "expected ')' after VALUES list, got {:?}",
19611                    self.peek()
19612                )));
19613            }
19614            self.advance();
19615            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19616            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19617            return Ok(TableRef {
19618                name,
19619                alias: alias_ident,
19620                only: false,
19621                as_of_segment: None,
19622                unnest_expr: None,
19623                unnest_column_aliases: column_aliases,
19624                with_ordinality: false,
19625                generate_series_args: None,
19626                lateral_subquery: Some(Box::new(head)),
19627                jsonb_each_text_arg: None,
19628                table_fn_call: None,
19629                rows_from: None,
19630                json_table: None,
19631                scalar_fn_item: false,
19632            });
19633        }
19634        // v7.37.17 (17.6 siblings) — plain derived table:
19635        // `FROM ( SELECT … ) [AS] alias`. Rides the same
19636        // lateral_subquery channel the explicit LATERAL form uses —
19637        // an uncorrelated inner SELECT executes identically. The
19638        // inner parse carries UNION tails (they live on
19639        // SelectStatement.unions).
19640        // v7.37 D.20 — the derived-table inner may itself be a
19641        // parenthesized set-operation group (`FROM ((SELECT…) UNION
19642        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19643        // bare `(SELECT …)`. parse_one_statement already routes a leading
19644        // `(` set-op group (its LParen arm) and a leading WITH
19645        // (parse_with_cte_then_select), so widen the second-token gate to
19646        // Select | LParen | WITH.
19647        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19648        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19649        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19650        // has existed since the shorthand landed and `parse_bare_select`
19651        // already routes it ("valid anywhere a SELECT head is"); what was
19652        // missing is this second-token gate, and the CTE body's dispatch
19653        // below. Round 868 found both by putting the shorthand in a
19654        // subquery — the top-level forms had been the only ones tested.
19655        if matches!(self.peek(), Token::LParen)
19656            && (matches!(
19657                self.tokens.get(self.pos + 1),
19658                Some(Token::Select | Token::LParen | Token::Table)
19659            ) || matches!(self.tokens.get(self.pos + 1),
19660                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19661        {
19662            self.advance(); // (
19663            let inner = match self.parse_one_statement()? {
19664                Statement::Select(s) => s,
19665                other => {
19666                    return Err(self.err(alloc::format!(
19667                        "expected SELECT inside derived table ( … ), got {other:?}"
19668                    )));
19669                }
19670            };
19671            if !matches!(self.peek(), Token::RParen) {
19672                return Err(self.err(alloc::format!(
19673                    "expected ')' after derived-table subquery, got {:?}",
19674                    self.peek()
19675                )));
19676            }
19677            self.advance();
19678            // `AS t(a, b)` column-alias list rides the
19679            // unnest_column_aliases field (same positional-rename
19680            // contract the unnest SRFs use).
19681            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19682            let name = alias_ident
19683                .clone()
19684                .unwrap_or_else(|| "subquery".to_string());
19685            return Ok(TableRef {
19686                name,
19687                alias: alias_ident,
19688                only: false,
19689                as_of_segment: None,
19690                unnest_expr: None,
19691                unnest_column_aliases: column_aliases,
19692                with_ordinality: false,
19693                generate_series_args: None,
19694                lateral_subquery: Some(Box::new(inner)),
19695                jsonb_each_text_arg: None,
19696                table_fn_call: None,
19697                rows_from: None,
19698                json_table: None,
19699                scalar_fn_item: false,
19700            });
19701        }
19702        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19703            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19704        {
19705            self.advance(); // LATERAL
19706            self.advance(); // (
19707            // Parse the inner SELECT.
19708            let inner = match self.parse_one_statement()? {
19709                Statement::Select(s) => s,
19710                other => {
19711                    return Err(self.err(alloc::format!(
19712                        "expected SELECT inside LATERAL ( … ), got {other:?}"
19713                    )));
19714                }
19715            };
19716            if !matches!(self.peek(), Token::RParen) {
19717                return Err(self.err(alloc::format!(
19718                    "expected ')' after LATERAL subquery, got {:?}",
19719                    self.peek()
19720                )));
19721            }
19722            self.advance();
19723            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19724            // `(VALUES …) t(g)` derived table round-trips through view-body
19725            // Display, which renders on the lateral_subquery channel).
19726            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19727            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19728            return Ok(TableRef {
19729                name,
19730                alias: alias_ident,
19731                only: false,
19732                as_of_segment: None,
19733                unnest_expr: None,
19734                unnest_column_aliases: column_aliases,
19735                with_ordinality: false,
19736                generate_series_args: None,
19737                lateral_subquery: Some(Box::new(inner)),
19738                jsonb_each_text_arg: None,
19739                table_fn_call: None,
19740                rows_from: None,
19741                json_table: None,
19742                scalar_fn_item: false,
19743            });
19744        }
19745        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19746        // function as a FROM item. Emits one row per (key, value)
19747        // pair in the JSONB object argument as TEXT columns. May
19748        // be wrapped in CROSS JOIN LATERAL when the argument
19749        // references a preceding FROM item (sentori migration
19750        // 0067 backfill shape: `CROSS JOIN LATERAL
19751        // jsonb_each_text(t.json_col) AS kv(key, value)`).
19752        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19753            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19754        {
19755            let each_fn = match self.peek() {
19756                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19757                _ => unreachable!(),
19758            };
19759            self.advance(); // jsonb_each[_text] / json_each[_text]
19760            self.advance(); // (
19761            let arg = self.parse_expr(0)?;
19762            if !matches!(self.peek(), Token::RParen) {
19763                return Err(self.err(alloc::format!(
19764                    "expected ')' after {each_fn}() argument, got {:?}",
19765                    self.peek()
19766                )));
19767            }
19768            self.advance();
19769            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19770            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19771            return Ok(TableRef {
19772                name,
19773                alias: alias_ident,
19774                only: false,
19775                as_of_segment: None,
19776                unnest_expr: None,
19777                // `AS t(k, v)` renames key/value positionally, same as the
19778                // LATERAL-position form already does.
19779                unnest_column_aliases: column_aliases,
19780                with_ordinality: false,
19781                generate_series_args: None,
19782                lateral_subquery: None,
19783                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19784                table_fn_call: None,
19785                rows_from: None,
19786                json_table: None,
19787                scalar_fn_item: false,
19788            });
19789        }
19790        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19791        // (+ json_ variants) — record-returning JSON functions with a
19792        // column-definition list. Desugar to a derived table that
19793        // projects each declared column from the JSON via `->>` + a cast,
19794        // over `jsonb_array_elements(J)` for the *set (per-element) form.
19795        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19796            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19797        {
19798            return self.parse_json_to_record_from();
19799        }
19800        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19801        // row is a text[] of capture groups, so it cannot desugar to unnest
19802        // (that would flatten the array). Wrap it as a derived table
19803        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19804        // SRF path already emits one text[] row per match. PG names the column
19805        // `regexp_matches`; an `AS a(col)` alias overrides it.
19806        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19807                if s.eq_ignore_ascii_case("regexp_matches"))
19808            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19809        {
19810            self.advance(); // fn name
19811            self.advance(); // (
19812            let mut fn_args: Vec<Expr> = Vec::new();
19813            loop {
19814                fn_args.push(self.parse_expr(0)?);
19815                if matches!(self.peek(), Token::Comma) {
19816                    self.advance();
19817                    continue;
19818                }
19819                break;
19820            }
19821            if !matches!(self.peek(), Token::RParen) {
19822                return Err(self.err(alloc::format!(
19823                    "expected ')' after regexp_matches() arguments, got {:?}",
19824                    self.peek()
19825                )));
19826            }
19827            self.advance();
19828            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19829            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19830            // it, so it died on the `with` token while every other table function
19831            // accepted it.
19832            let with_ordinality = self.absorb_with_ordinality();
19833            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19834            let table_alias = alias_ident
19835                .clone()
19836                .unwrap_or_else(|| "regexp_matches".to_string());
19837            // PG names a single-column function's output column after the ALIAS
19838            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19839            // `m` reads as that column and not as a whole-row composite. Naming
19840            // it after the function regardless made `SELECT m[1] FROM … AS m`
19841            // subscript a record.
19842            let col_name = column_aliases
19843                .first()
19844                .cloned()
19845                .or_else(|| alias_ident.clone())
19846                .unwrap_or_else(|| "regexp_matches".to_string());
19847            let inner = crate::ast::SelectStatement {
19848                locking: None,
19849                ctes: Vec::new(),
19850                distinct: false,
19851                distinct_on: Vec::new(),
19852                items: alloc::vec![SelectItem::Expr {
19853                    expr: Expr::FunctionCall {
19854                        name: "regexp_matches".to_string(),
19855                        args: fn_args,
19856                    },
19857                    alias: Some(col_name),
19858                }],
19859                from: None,
19860                where_: None,
19861                group_by: None,
19862                group_by_all: false,
19863                having: None,
19864                unions: Vec::new(),
19865                order_by: Vec::new(),
19866                limit: None,
19867                offset: None,
19868                limit_with_ties: false,
19869                window_check_exprs: Vec::new(),
19870            };
19871            return Ok(TableRef {
19872                name: table_alias.clone(),
19873                alias: Some(table_alias),
19874                only: false,
19875                as_of_segment: None,
19876                unnest_expr: None,
19877                unnest_column_aliases: column_aliases,
19878                with_ordinality,
19879                generate_series_args: None,
19880                lateral_subquery: Some(Box::new(inner)),
19881                jsonb_each_text_arg: None,
19882                table_fn_call: None,
19883                rows_from: None,
19884                json_table: None,
19885                // regexp_matches returns text[], a base type: `SELECT m FROM
19886                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19887                scalar_fn_item: true,
19888            });
19889        }
19890        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19891        // / json_ variants as a FROM item. Rewritten into
19892        // `unnest(<same fn>(<expr>))`: the scalar form returns the
19893        // elements as a TEXT array, and the existing unnest SRF path
19894        // materialises one row per element. PG's natural column name
19895        // is `value`; an `AS a(col)` column-alias list overrides it.
19896        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19897                if s.eq_ignore_ascii_case("jsonb_array_elements")
19898                    || s.eq_ignore_ascii_case("json_array_elements")
19899                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19900                    || s.eq_ignore_ascii_case("json_array_elements_text")
19901                    || s.eq_ignore_ascii_case("jsonb_object_keys")
19902                    || s.eq_ignore_ascii_case("json_object_keys")
19903                    || s.eq_ignore_ascii_case("jsonb_path_query")
19904                    || s.eq_ignore_ascii_case("json_path_query")
19905                    || s.eq_ignore_ascii_case("generate_subscripts")
19906                    || s.eq_ignore_ascii_case("string_to_table")
19907                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
19908            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19909        {
19910            let fn_name = match self.peek() {
19911                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19912                _ => unreachable!(),
19913            };
19914            self.advance(); // fn name
19915            self.advance(); // (
19916            let mut fn_args: Vec<Expr> = Vec::new();
19917            loop {
19918                fn_args.push(self.parse_expr(0)?);
19919                if matches!(self.peek(), Token::Comma) {
19920                    self.advance();
19921                    continue;
19922                }
19923                break;
19924            }
19925            if !matches!(self.peek(), Token::RParen) {
19926                return Err(self.err(alloc::format!(
19927                    "expected ')' after {fn_name}() arguments, got {:?}",
19928                    self.peek()
19929                )));
19930            }
19931            self.advance();
19932            let with_ordinality = self.absorb_with_ordinality();
19933            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19934            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19935            // PG's natural column name: the array-elements SRFs
19936            // declare an OUT parameter `value`; jsonb_object_keys
19937            // and generate_subscripts have none, so the column is
19938            // named after the function. A bare table alias on a
19939            // single-column SRF renames the column too (PG: `FROM
19940            // generate_subscripts(a, 1) AS s` projects column s) —
19941            // except for the OUT-parameter SRFs, whose column stays
19942            // `value` under a bare alias.
19943            let natural_col = if fn_name.ends_with("_array_elements")
19944                || fn_name.ends_with("_array_elements_text")
19945            {
19946                "value".to_string()
19947            } else {
19948                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19949            };
19950            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19951            // Keep any further entries — the second names the
19952            // ordinality column under WITH ORDINALITY.
19953            srf_cols.extend(column_aliases.into_iter().skip(1));
19954            // The *_to_table SRFs are row-streams over the existing
19955            // *_to_array scalars — map the call target; the display
19956            // name (alias / column defaults) keeps the SRF spelling.
19957            let call_name = match fn_name.as_str() {
19958                "string_to_table" => "string_to_array".to_string(),
19959                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
19960                _ => fn_name,
19961            };
19962            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
19963            // preceding FROM item (bare or qualified column) is correlated;
19964            // route it through the per-outer-row lateral channel.
19965            let expr = crate::ast::Expr::FunctionCall {
19966                name: call_name,
19967                args: fn_args,
19968            };
19969            let correlated = Self::expr_has_any_column(&expr);
19970            let tref = TableRef {
19971                name,
19972                alias: alias_ident,
19973                only: false,
19974                as_of_segment: None,
19975                unnest_expr: Some(Box::new(expr)),
19976                unnest_column_aliases: srf_cols,
19977                with_ordinality,
19978                generate_series_args: None,
19979                lateral_subquery: None,
19980                jsonb_each_text_arg: None,
19981                table_fn_call: None,
19982                rows_from: None,
19983                json_table: None,
19984                // Each of these returns a BASE type (jsonb / text / int), so the item's
19985                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
19986                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
19987                scalar_fn_item: !with_ordinality,
19988            };
19989            return Ok(if correlated {
19990                Self::wrap_correlated_srf(tref)
19991            } else {
19992                tref
19993            });
19994        }
19995        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
19996        // explicit parallel-zip syntax. Each entry lowers to its
19997        // array-returning scalar form (unnest(x) → x itself; the
19998        // FROM-SRF rewrite family → their scalar array calls) and
19999        // the list rides the multi-arg unnest zip channel:
20000        // NULL-padded to the longest, WITH ORDINALITY appends the
20001        // counter. generate_series has no scalar array form and
20002        // errors honestly.
20003        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
20004            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
20005            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
20006        {
20007            self.advance(); // ROWS
20008            self.advance(); // FROM
20009            self.advance(); // (
20010            let mut entries: Vec<Expr> = Vec::new();
20011            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
20012            // Used only when some entry has no array form.
20013            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
20014            loop {
20015                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
20016                if !matches!(self.peek(), Token::LParen) {
20017                    return Err(self.err(alloc::format!(
20018                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
20019                        self.peek()
20020                    )));
20021                }
20022                self.advance();
20023                let mut fn_args: Vec<Expr> = Vec::new();
20024                if !matches!(self.peek(), Token::RParen) {
20025                    loop {
20026                        fn_args.push(self.parse_expr(0)?);
20027                        if matches!(self.peek(), Token::Comma) {
20028                            self.advance();
20029                            continue;
20030                        }
20031                        break;
20032                    }
20033                }
20034                if !matches!(self.peek(), Token::RParen) {
20035                    return Err(self.err(alloc::format!(
20036                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
20037                        self.peek()
20038                    )));
20039                }
20040                self.advance();
20041                let entry = match fn_name.as_str() {
20042                    "unnest" => {
20043                        if fn_args.len() != 1 {
20044                            return Err(
20045                                self.err("unnest inside ROWS FROM takes exactly one array".into())
20046                            );
20047                        }
20048                        fn_args.pop().expect("len checked")
20049                    }
20050                    "jsonb_array_elements"
20051                    | "json_array_elements"
20052                    | "jsonb_array_elements_text"
20053                    | "json_array_elements_text"
20054                    | "jsonb_object_keys"
20055                    | "json_object_keys"
20056                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
20057                        name: fn_name,
20058                        args: fn_args,
20059                    },
20060                    "string_to_table" => crate::ast::Expr::FunctionCall {
20061                        name: "string_to_array".to_string(),
20062                        args: fn_args,
20063                    },
20064                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
20065                        name: "regexp_split_to_array".to_string(),
20066                        args: fn_args,
20067                    },
20068                    // v7.39 (read01 round 74) — an SRF with no array form
20069                    // (`generate_series`, a user `RETURNS SETOF` function) has no
20070                    // scalar expression to zip, so the WHOLE list switches to the
20071                    // rows_from channel, which runs each function and zips the
20072                    // rows themselves. The all-array case keeps the old lowering:
20073                    // it is well-trodden and this must not disturb it.
20074                    _ => {
20075                        generic.push((fn_name, fn_args));
20076                        if matches!(self.peek(), Token::Comma) {
20077                            self.advance();
20078                            continue;
20079                        }
20080                        break;
20081                    }
20082                };
20083                generic.push((
20084                    // The array-able entries carry their lowered expr along, so a
20085                    // MIXED list still works: the engine sees the scalar array
20086                    // form and unnests it.
20087                    "__array".to_string(),
20088                    alloc::vec![entry.clone()],
20089                ));
20090                entries.push(entry);
20091                if matches!(self.peek(), Token::Comma) {
20092                    self.advance();
20093                    continue;
20094                }
20095                break;
20096            }
20097            if !matches!(self.peek(), Token::RParen) {
20098                return Err(self.err(alloc::format!(
20099                    "expected ')' to close ROWS FROM, got {:?}",
20100                    self.peek()
20101                )));
20102            }
20103            self.advance();
20104            let with_ordinality = self.absorb_with_ordinality();
20105            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20106            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
20107            // v7.39 (read01 round 74) — some entry had no array form, so the whole
20108            // list rides the generic channel.
20109            if generic.iter().any(|(n, _)| n != "__array") {
20110                let correlated = generic
20111                    .iter()
20112                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
20113                let tref = TableRef {
20114                    name,
20115                    alias: alias_ident,
20116                    only: false,
20117                    as_of_segment: None,
20118                    unnest_expr: None,
20119                    unnest_column_aliases,
20120                    with_ordinality,
20121                    generate_series_args: None,
20122                    lateral_subquery: None,
20123                    jsonb_each_text_arg: None,
20124                    table_fn_call: None,
20125                    rows_from: Some(generic),
20126                    json_table: None,
20127                    scalar_fn_item: false,
20128                };
20129                return Ok(if correlated {
20130                    Self::wrap_correlated_srf(tref)
20131                } else {
20132                    tref
20133                });
20134            }
20135            let correlated = entries.iter().any(Self::expr_has_any_column);
20136            let expr = if entries.len() == 1 {
20137                entries.pop().expect("len checked")
20138            } else {
20139                crate::ast::Expr::FunctionCall {
20140                    name: "__unnest_zip".to_string(),
20141                    args: entries,
20142                }
20143            };
20144            let tref = TableRef {
20145                name,
20146                alias: alias_ident,
20147                only: false,
20148                as_of_segment: None,
20149                unnest_expr: Some(Box::new(expr)),
20150                unnest_column_aliases,
20151                with_ordinality,
20152                generate_series_args: None,
20153                lateral_subquery: None,
20154                jsonb_each_text_arg: None,
20155                table_fn_call: None,
20156                rows_from: None,
20157                json_table: None,
20158                scalar_fn_item: false,
20159            };
20160            return Ok(if correlated {
20161                Self::wrap_correlated_srf(tref)
20162            } else {
20163                tref
20164            });
20165        }
20166        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
20167        // source. Detect at the head before the bare-ident fallback;
20168        // unnest is not a reserved token.
20169        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
20170            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20171        {
20172            self.advance(); // unnest
20173            self.advance(); // (
20174            let mut srf_args = alloc::vec![self.parse_expr(0)?];
20175            while matches!(self.peek(), Token::Comma) {
20176                self.advance();
20177                srf_args.push(self.parse_expr(0)?);
20178            }
20179            if !matches!(self.peek(), Token::RParen) {
20180                return Err(self.err(alloc::format!(
20181                    "expected ')' after unnest() argument, got {:?}",
20182                    self.peek()
20183                )));
20184            }
20185            self.advance();
20186            // Multi-arg unnest(a, b, …) zips the arrays in
20187            // parallel, NULL-padding to the longest (PG's ROWS
20188            // FROM shorthand). Lower onto the unnest channel as an
20189            // internal marker call the executors unpack.
20190            let expr = if srf_args.len() == 1 {
20191                srf_args.pop().expect("len checked")
20192            } else {
20193                crate::ast::Expr::FunctionCall {
20194                    name: "__unnest_zip".to_string(),
20195                    args: srf_args,
20196                }
20197            };
20198            let with_ordinality = self.absorb_with_ordinality();
20199            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20200            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
20201            let correlated = Self::expr_has_any_column(&expr);
20202            let tref = TableRef {
20203                name,
20204                alias: alias_ident,
20205                only: false,
20206                as_of_segment: None,
20207                unnest_expr: Some(Box::new(expr)),
20208                unnest_column_aliases,
20209                with_ordinality,
20210                generate_series_args: None,
20211                lateral_subquery: None,
20212                jsonb_each_text_arg: None,
20213                table_fn_call: None,
20214                rows_from: None,
20215                json_table: None,
20216                scalar_fn_item: false,
20217            };
20218            return Ok(if correlated {
20219                Self::wrap_correlated_srf(tref)
20220            } else {
20221                tref
20222            });
20223        }
20224        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
20225        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
20226        // generic table-fn arg parser can't read), so it is intercepted
20227        // here BEFORE the generic dispatch. The doc expr may reference
20228        // outer columns (implicit LATERAL) — same correlated-wrap rule.
20229        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20230                if s.eq_ignore_ascii_case("json_table"))
20231            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20232        {
20233            let tref = self.parse_json_table_ref()?;
20234            let correlated = tref
20235                .json_table
20236                .as_deref()
20237                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
20238            return Ok(if correlated {
20239                Self::wrap_correlated_srf(tref)
20240            } else {
20241                tref
20242            });
20243        }
20244        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
20245        // functions dispatched by name (`pg_partition_tree('t')`,
20246        // `pg_partition_ancestors('t')`). Same head-detection shape as
20247        // unnest; the engine executor owns the row shape per function.
20248        // v7.39 (read01 round 65) — and a USER function in FROM position
20249        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
20250        // (generate_series / unnest / the json_each family) keep it — their arms
20251        // sit further down, so they are excluded here by name rather than by
20252        // ordering. Anything else that is an ident followed by `(` is a table
20253        // function; the engine executor decides whether it is a builtin, a
20254        // set-returning user function, or an error.
20255        // 7.38.1 S5.1 — pg_dump spells its table functions
20256        // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
20257        // strip the pg_catalog prefix here so the same head-detection
20258        // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
20259        // meaning.
20260        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
20261            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
20262            && matches!(
20263                self.tokens.get(self.pos + 2),
20264                Some(Token::Ident(_) | Token::QuotedIdent(_))
20265            )
20266            && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
20267        {
20268            self.advance(); // pg_catalog
20269            self.advance(); // .
20270        }
20271        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20272                if !s.eq_ignore_ascii_case("generate_series")
20273                    && !s.eq_ignore_ascii_case("unnest")
20274                    && !is_json_each_name(s))
20275            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20276        {
20277            // Body out-of-line — this parse sits on the FROM/subquery
20278            // recursion chain (debug frame-cliff discipline).
20279            // v7.39 (read01 round 69) — a call whose arguments reference an outer
20280            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
20281            // outer row, so it rides the lateral channel. Same rule the unnest
20282            // arm uses.
20283            let tref = self.parse_table_fn_ref()?;
20284            let correlated = tref
20285                .table_fn_call
20286                .as_deref()
20287                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
20288            return Ok(if correlated {
20289                Self::wrap_correlated_srf(tref)
20290            } else {
20291                tref
20292            });
20293        }
20294        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
20295        // [, step])` set-returning source. Same shape as unnest:
20296        // detect at the head, parse the comma-separated arg list,
20297        // dispatch downstream through the engine's set-returning
20298        // path. Supports integer triplets (mailrs's `WITH row_no AS
20299        // (SELECT * FROM generate_series(1, N))` pattern) and
20300        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
20301        // date-range iteration pattern, which pre-3.10 had no
20302        // direct equivalent in SPG).
20303        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
20304            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20305        {
20306            self.advance(); // generate_series
20307            self.advance(); // (
20308            let mut args: Vec<Expr> = Vec::new();
20309            loop {
20310                args.push(self.parse_expr(0)?);
20311                if matches!(self.peek(), Token::Comma) {
20312                    self.advance();
20313                    continue;
20314                }
20315                break;
20316            }
20317            if !matches!(self.peek(), Token::RParen) {
20318                return Err(self.err(alloc::format!(
20319                    "expected ')' after generate_series() arguments, got {:?}",
20320                    self.peek()
20321                )));
20322            }
20323            self.advance();
20324            if args.len() < 2 || args.len() > 3 {
20325                return Err(self.err(alloc::format!(
20326                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
20327                    args.len()
20328                )));
20329            }
20330            let with_ordinality = self.absorb_with_ordinality();
20331            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
20332            let name = alias_ident
20333                .clone()
20334                .unwrap_or_else(|| "generate_series".to_string());
20335            let correlated = args.iter().any(Self::expr_has_any_column);
20336            let tref = TableRef {
20337                name,
20338                alias: alias_ident,
20339                only: false,
20340                as_of_segment: None,
20341                unnest_expr: None,
20342                unnest_column_aliases: column_aliases,
20343                with_ordinality,
20344                generate_series_args: Some(args),
20345                lateral_subquery: None,
20346                jsonb_each_text_arg: None,
20347                table_fn_call: None,
20348                rows_from: None,
20349                json_table: None,
20350                scalar_fn_item: false,
20351            };
20352            return Ok(if correlated {
20353                Self::wrap_correlated_srf(tref)
20354            } else {
20355                tref
20356            });
20357        }
20358        // v7.16.2 — preserve information_schema / pg_catalog
20359        // qualifiers (mailrs round-10 A.3). The generic
20360        // `expect_ident_like` strip silently drops the schema;
20361        // we want the engine to recognise these PG meta tables
20362        // and synthesise rows from the live catalog. Produce a
20363        // synthetic name (`__spg_info_columns` etc.) so the
20364        // engine's SELECT-side router can dispatch without
20365        // clashing with any user-defined `columns` table.
20366        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
20367            (synth, Some(orig))
20368        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
20369            (synth, Some(orig))
20370        } else {
20371            (self.expect_ident_like()?, None)
20372        };
20373        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
20374        // time-travel clause. Parse BEFORE the alias so the
20375        // alias can still ride at the tail (`tbl AS OF SEGMENT
20376        // '5' alias`). `AS` is a reserved keyword token, while
20377        // `OF` and `SEGMENT` are bare idents.
20378        let as_of_segment = if matches!(self.peek(), Token::As)
20379            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
20380        {
20381            self.advance(); // AS
20382            self.advance(); // OF
20383            let kw = match self.peek().clone() {
20384                Token::Ident(s) | Token::QuotedIdent(s) => s,
20385                other => {
20386                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
20387                }
20388            };
20389            if !kw.eq_ignore_ascii_case("segment") {
20390                return Err(self.err(format!(
20391                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
20392                )));
20393            }
20394            self.advance();
20395            // Segment id literal — accept either a string or
20396            // integer for operator ergonomics.
20397            let id = match self.advance() {
20398                Token::String(s) => s
20399                    .parse::<u32>()
20400                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20401                Token::Integer(n) => u32::try_from(n)
20402                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20403                other => {
20404                    return Err(self.err(format!(
20405                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
20406                    )));
20407                }
20408            };
20409            Some(id)
20410        } else {
20411            None
20412        };
20413        // TABLESAMPLE is not a reserved token — keep the bare-ident
20414        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20415        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20416        {
20417            None
20418        } else {
20419            self.parse_optional_alias()?
20420        };
20421        // r1052 — a catalog name rewritten to its synthetic form keeps
20422        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20423        // still binds after `pg_cast` became `__spg_pg_cast`. PG
20424        // semantics: the visible name of `pg_catalog.pg_cast` IS
20425        // `pg_cast`. Without this, every table-name-qualified column
20426        // on a synthesised catalog answered "missing FROM-clause
20427        // entry" — which is the wall pg_dump hit on its first
20428        // pg_proc/pg_cast query.
20429        let alias = match (&alias, &meta_original) {
20430            (None, Some(orig)) if *orig != name => Some(orig.clone()),
20431            _ => alias,
20432        };
20433        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20434        // (PG grammar). BERNOULLI lowers to a per-row
20435        // `random() < p/100` conjunct on the enclosing SELECT's
20436        // WHERE — exact row-level Bernoulli semantics. SYSTEM
20437        // shares the lowering: SPG has no page structure to
20438        // sample, and the row-level form returns the same expected
20439        // fraction. REPEATABLE(seed) promises a deterministic
20440        // sample SPG cannot honour yet — honest error rather than
20441        // a silently ignored seed.
20442        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20443            self.advance();
20444            let method = self.expect_ident_like()?;
20445            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20446                return Err(self.err(alloc::format!(
20447                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20448                )));
20449            }
20450            if !matches!(self.peek(), Token::LParen) {
20451                return Err(self.err(alloc::format!(
20452                    "expected '(' after TABLESAMPLE {}, got {:?}",
20453                    method.to_ascii_uppercase(),
20454                    self.peek()
20455                )));
20456            }
20457            self.advance();
20458            let percent = self.parse_expr(0)?;
20459            if !matches!(self.peek(), Token::RParen) {
20460                return Err(self.err(alloc::format!(
20461                    "expected ')' after TABLESAMPLE percentage, got {:?}",
20462                    self.peek()
20463                )));
20464            }
20465            self.advance();
20466            // REPEATABLE(seed) → a deterministic per-row draw seeded by
20467            // `seed`, so the sample is stable across repeats and rescans.
20468            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20469            let mut sample_seed: Option<Expr> = None;
20470            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20471                self.advance();
20472                if !matches!(self.peek(), Token::LParen) {
20473                    return Err(self.err(alloc::format!(
20474                        "expected '(' after REPEATABLE, got {:?}",
20475                        self.peek()
20476                    )));
20477                }
20478                self.advance();
20479                let seed = self.parse_expr(0)?;
20480                if !matches!(self.peek(), Token::RParen) {
20481                    return Err(self.err(alloc::format!(
20482                        "expected ')' after REPEATABLE seed, got {:?}",
20483                        self.peek()
20484                    )));
20485                }
20486                self.advance();
20487                sample_seed = Some(seed);
20488            }
20489            let draw = match sample_seed {
20490                Some(seed) => Expr::FunctionCall {
20491                    name: "__tsm_fract".to_string(),
20492                    args: alloc::vec![seed],
20493                },
20494                None => Expr::FunctionCall {
20495                    name: "random".to_string(),
20496                    args: Vec::new(),
20497                },
20498            };
20499            self.pending_sample_preds.push(Expr::Binary {
20500                lhs: Box::new(draw),
20501                op: crate::ast::BinOp::Lt,
20502                rhs: Box::new(Expr::Binary {
20503                    lhs: Box::new(percent),
20504                    op: crate::ast::BinOp::Div,
20505                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20506                }),
20507            });
20508        }
20509        Ok(TableRef {
20510            name,
20511            alias,
20512            only,
20513            as_of_segment,
20514            unnest_expr: None,
20515            unnest_column_aliases: Vec::new(),
20516            with_ordinality: false,
20517            generate_series_args: None,
20518            lateral_subquery: None,
20519            jsonb_each_text_arg: None,
20520            table_fn_call: None,
20521            rows_from: None,
20522            json_table: None,
20523            scalar_fn_item: false,
20524        })
20525    }
20526
20527    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20528    /// but also accepts `AS alias(col [, col, …])` — the
20529    /// PG-standard table-function column-list form. The column
20530    /// list is only honoured when paired with `UNNEST(...)` in
20531    /// the parent; other call sites currently discard it.
20532    /// True when the expression tree contains a qualified column
20533    /// reference (`t.col`) — the syntactic marker that an SRF
20534    /// argument correlates with a preceding FROM item.
20535    fn expr_has_qualified_column(e: &Expr) -> bool {
20536        match e {
20537            Expr::Column(c) => c.qualifier.is_some(),
20538            Expr::Binary { lhs, rhs, .. } => {
20539                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20540            }
20541            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20542            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20543            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20544            Expr::Case {
20545                operand,
20546                branches,
20547                else_branch,
20548            } => {
20549                operand
20550                    .as_deref()
20551                    .is_some_and(Self::expr_has_qualified_column)
20552                    || branches.iter().any(|(w, t)| {
20553                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20554                    })
20555                    || else_branch
20556                        .as_deref()
20557                        .is_some_and(Self::expr_has_qualified_column)
20558            }
20559            _ => false,
20560        }
20561    }
20562
20563    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20564    /// counts a bare (unqualified) column. A set-returning function has no
20565    /// input columns of its own, so ANY column in its arguments is an outer
20566    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20567    fn expr_has_any_column(e: &Expr) -> bool {
20568        match e {
20569            Expr::Column(_) => true,
20570            Expr::Binary { lhs, rhs, .. } => {
20571                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20572            }
20573            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20574            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20575            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20576            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20577            // constructor or subscript fell to the `_ => false` arm, so
20578            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20579            // channel and the eager peer eval answered `column "x" does
20580            // not exist` (the substitution walker already recurses both
20581            // shapes; only this detector was blind to them).
20582            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20583            Expr::ArraySubscript { target, index } => {
20584                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20585            }
20586            Expr::Case {
20587                operand,
20588                branches,
20589                else_branch,
20590            } => {
20591                operand.as_deref().is_some_and(Self::expr_has_any_column)
20592                    || branches
20593                        .iter()
20594                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20595                    || else_branch
20596                        .as_deref()
20597                        .is_some_and(Self::expr_has_any_column)
20598            }
20599            _ => false,
20600        }
20601    }
20602
20603    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20604    /// `generate_series(1, t.n)`) into the lateral_subquery
20605    /// channel: `SELECT * FROM <srf>` executes per outer row with
20606    /// outer references substituted (v7.37.43-T4.5 machinery).
20607    /// Uncorrelated SRFs stay on their plain channels.
20608    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20609        let name = srf.name.clone();
20610        let alias = srf.alias.clone();
20611        let inner = crate::ast::SelectStatement {
20612            locking: None,
20613            ctes: Vec::new(),
20614            distinct: false,
20615            distinct_on: Vec::new(),
20616            items: alloc::vec![crate::ast::SelectItem::Wildcard],
20617            from: Some(crate::ast::FromClause {
20618                primary: srf,
20619                joins: Vec::new(),
20620            }),
20621            where_: None,
20622            group_by: None,
20623            group_by_all: false,
20624            having: None,
20625            unions: Vec::new(),
20626            order_by: Vec::new(),
20627            limit: None,
20628            offset: None,
20629            limit_with_ties: false,
20630            window_check_exprs: Vec::new(),
20631        };
20632        TableRef {
20633            name,
20634            alias,
20635            only: false,
20636            as_of_segment: None,
20637            unnest_expr: None,
20638            unnest_column_aliases: Vec::new(),
20639            with_ordinality: false,
20640            generate_series_args: None,
20641            lateral_subquery: Some(Box::new(inner)),
20642            jsonb_each_text_arg: None,
20643            table_fn_call: None,
20644            rows_from: None,
20645            json_table: None,
20646            scalar_fn_item: false,
20647        }
20648    }
20649
20650    /// True when the expression tree contains an unresolved
20651    /// `OVER w` marker (see parse_over_clause).
20652    fn expr_has_named_window(e: &Expr) -> bool {
20653        match e {
20654            Expr::WindowFunction { partition_by, .. } => matches!(
20655                partition_by.as_slice(),
20656                [Expr::Column(c)] if matches!(
20657                    c.qualifier.as_deref(),
20658                    Some("__named_window__") | Some("__named_window_ref__")
20659                )
20660            ),
20661            Expr::Binary { lhs, rhs, .. } => {
20662                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20663            }
20664            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20665            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20666            Expr::Case {
20667                operand,
20668                branches,
20669                else_branch,
20670            } => {
20671                operand.as_deref().is_some_and(Self::expr_has_named_window)
20672                    || branches.iter().any(|(w, t)| {
20673                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20674                    })
20675                    || else_branch
20676                        .as_deref()
20677                        .is_some_and(Self::expr_has_named_window)
20678            }
20679            _ => false,
20680        }
20681    }
20682
20683    /// v7.39 (round 705) — the NAMES the expression references through the
20684    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20685    /// definitions nothing referenced. Traversal mirrors
20686    /// `expr_has_named_window` above.
20687    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20688        match e {
20689            Expr::WindowFunction { partition_by, .. } => {
20690                if let [Expr::Column(c)] = partition_by.as_slice()
20691                    && matches!(
20692                        c.qualifier.as_deref(),
20693                        Some("__named_window__") | Some("__named_window_ref__")
20694                    )
20695                {
20696                    into.push(c.name.clone());
20697                }
20698            }
20699            Expr::Binary { lhs, rhs, .. } => {
20700                Self::collect_named_window_refs(lhs, into);
20701                Self::collect_named_window_refs(rhs, into);
20702            }
20703            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20704                Self::collect_named_window_refs(expr, into);
20705            }
20706            Expr::FunctionCall { args, .. } => {
20707                for a in args {
20708                    Self::collect_named_window_refs(a, into);
20709                }
20710            }
20711            Expr::Case {
20712                operand,
20713                branches,
20714                else_branch,
20715            } => {
20716                if let Some(o) = operand.as_deref() {
20717                    Self::collect_named_window_refs(o, into);
20718                }
20719                for (w, t) in branches {
20720                    Self::collect_named_window_refs(w, into);
20721                    Self::collect_named_window_refs(t, into);
20722                }
20723                if let Some(eb) = else_branch.as_deref() {
20724                    Self::collect_named_window_refs(eb, into);
20725                }
20726            }
20727            _ => {}
20728        }
20729    }
20730
20731    /// Inline named-window definitions into the `OVER w` markers.
20732    /// An unknown name errors (PG: window "w" does not exist).
20733    #[allow(clippy::type_complexity)]
20734    fn substitute_named_windows(
20735        e: &mut Expr,
20736        defs: &[(
20737            String,
20738            (
20739                Vec<Expr>,
20740                Vec<(Expr, bool, Option<bool>)>,
20741                Option<WindowFrame>,
20742            ),
20743        )],
20744    ) -> Result<(), String> {
20745        match e {
20746            Expr::WindowFunction {
20747                partition_by,
20748                order_by,
20749                frame,
20750                ..
20751            } => {
20752                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20753                // from the bare `OVER w1` (a plain reference).
20754                let named = match partition_by.as_slice() {
20755                    [Expr::Column(c)] => match c.qualifier.as_deref() {
20756                        Some("__named_window__") => Some((c.name.clone(), false)),
20757                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
20758                        _ => None,
20759                    },
20760                    _ => None,
20761                };
20762                if let Some((wname, is_copy)) = named {
20763                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20764                    else {
20765                        return Err(alloc::format!("window {wname:?} does not exist"));
20766                    };
20767                    if !is_copy {
20768                        *partition_by = def.0.clone();
20769                        *order_by = def.1.clone();
20770                        *frame = def.2.clone();
20771                        return Ok(());
20772                    }
20773                    // v7.39 (round 229) — PG's copy rules, probed against
20774                    // 18.4: a copy inherits the partitioning, may supply an
20775                    // ordering only when the base has none, and may not copy
20776                    // a base that already carries a frame (its own frame
20777                    // would be ambiguous with the inherited one).
20778                    if !def.1.is_empty() && !order_by.is_empty() {
20779                        return Err(alloc::format!(
20780                            "cannot override ORDER BY clause of window \"{wname}\""
20781                        ));
20782                    }
20783                    if def.2.is_some() {
20784                        return Err(alloc::format!(
20785                            "cannot copy window \"{wname}\" because it has a frame clause"
20786                        ));
20787                    }
20788                    *partition_by = def.0.clone();
20789                    if order_by.is_empty() {
20790                        *order_by = def.1.clone();
20791                    }
20792                }
20793                Ok(())
20794            }
20795            Expr::Binary { lhs, rhs, .. } => {
20796                Self::substitute_named_windows(lhs, defs)?;
20797                Self::substitute_named_windows(rhs, defs)
20798            }
20799            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20800                Self::substitute_named_windows(expr, defs)
20801            }
20802            Expr::FunctionCall { args, .. } => {
20803                for a in args {
20804                    Self::substitute_named_windows(a, defs)?;
20805                }
20806                Ok(())
20807            }
20808            Expr::Case {
20809                operand,
20810                branches,
20811                else_branch,
20812            } => {
20813                if let Some(op) = operand {
20814                    Self::substitute_named_windows(op, defs)?;
20815                }
20816                for (w, t) in branches {
20817                    Self::substitute_named_windows(w, defs)?;
20818                    Self::substitute_named_windows(t, defs)?;
20819                }
20820                if let Some(el) = else_branch {
20821                    Self::substitute_named_windows(el, defs)?;
20822                }
20823                Ok(())
20824            }
20825            _ => Ok(()),
20826        }
20827    }
20828
20829    /// SQL-standard `TABLE name` shorthand — builds the equivalent
20830    /// `SELECT * FROM name` head. Callers own set-op chain / tail
20831    /// composition.
20832    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20833        debug_assert!(matches!(self.peek(), Token::Table));
20834        self.advance(); // TABLE
20835        let tname = self.expect_ident_like()?;
20836        Ok(SelectStatement {
20837            locking: None,
20838            ctes: Vec::new(),
20839            distinct: false,
20840            distinct_on: Vec::new(),
20841            items: alloc::vec![SelectItem::Wildcard],
20842            from: Some(FromClause {
20843                primary: TableRef {
20844                    name: tname,
20845                    alias: None,
20846                    only: false,
20847                    as_of_segment: None,
20848                    unnest_expr: None,
20849                    unnest_column_aliases: Vec::new(),
20850                    with_ordinality: false,
20851                    generate_series_args: None,
20852                    lateral_subquery: None,
20853                    jsonb_each_text_arg: None,
20854                    table_fn_call: None,
20855                    rows_from: None,
20856                    json_table: None,
20857                    scalar_fn_item: false,
20858                },
20859                joins: Vec::new(),
20860            }),
20861            where_: None,
20862            group_by: None,
20863            group_by_all: false,
20864            having: None,
20865            unions: Vec::new(),
20866            order_by: Vec::new(),
20867            limit: None,
20868            offset: None,
20869            limit_with_ties: false,
20870            window_check_exprs: Vec::new(),
20871        })
20872    }
20873
20874    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20875    /// variants) → a derived table that reads each declared column out of
20876    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20877    /// `jsonb_array_elements(J)` (one row per element, column `value`);
20878    /// the scalar *record form projects a single row straight off `J`.
20879    /// Rides the existing lateral-subquery channel, so no new executor or
20880    /// AST is needed.
20881    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20882        use crate::ast::{
20883            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20884        };
20885        let fn_name = match self.peek() {
20886            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20887            _ => unreachable!("caller guarded is_json_to_record_name"),
20888        };
20889        self.advance(); // fn name
20890        self.advance(); // (
20891        let mut arg = self.parse_expr(0)?;
20892        // populate_record(base, json): the base only carries the record
20893        // type here — the JSON argument is the second expression.
20894        let mut base: Option<Expr> = None;
20895        if matches!(self.peek(), Token::Comma) {
20896            self.advance();
20897            base = Some(arg);
20898            arg = self.parse_expr(0)?;
20899        }
20900        if !matches!(self.peek(), Token::RParen) {
20901            return Err(self.err(alloc::format!(
20902                "expected ')' after {fn_name}() argument, got {:?}",
20903                self.peek()
20904            )));
20905        }
20906        self.advance(); // )
20907        let is_set = fn_name.ends_with("recordset");
20908        // `[AS] alias ( col type [, …] )` column-definition list.
20909        if matches!(self.peek(), Token::As) {
20910            self.advance();
20911        }
20912        let alias_opt = match self.peek() {
20913            Token::Ident(s) | Token::QuotedIdent(s) => {
20914                let a = s.clone();
20915                self.advance();
20916                Some(a)
20917            }
20918            _ => None,
20919        };
20920        // v7.39 (read01 round 76) — the populate family's canonical PG
20921        // spelling carries no column list at all: the row shape comes from
20922        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20923        // j)`). The parser has no catalog, so hand the two arguments to the
20924        // engine's table-function channel, which does. Only `*_to_record*`
20925        // (whose base is bare `record`) genuinely requires the list.
20926        if !matches!(self.peek(), Token::LParen) {
20927            if let Some(base_expr) = base {
20928                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20929                return Ok(TableRef {
20930                    name: alias.clone(),
20931                    alias: Some(alias),
20932                    only: false,
20933                    as_of_segment: None,
20934                    unnest_expr: None,
20935                    unnest_column_aliases: Vec::new(),
20936                    with_ordinality: false,
20937                    generate_series_args: None,
20938                    lateral_subquery: None,
20939                    jsonb_each_text_arg: None,
20940                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20941                    rows_from: None,
20942                    json_table: None,
20943                    scalar_fn_item: false,
20944                });
20945            }
20946            return Err(self.err(alloc::format!(
20947                "expected '(' to start the {fn_name} column-definition list, got {:?}",
20948                self.peek()
20949            )));
20950        }
20951        let Some(alias) = alias_opt else {
20952            return Err(self.err(alloc::format!(
20953                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20954            )));
20955        };
20956        self.advance(); // (
20957        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
20958        loop {
20959            let col = self.expect_ident_like()?;
20960            let ty = self.parse_cast_target()?;
20961            coldefs.push((col, ty));
20962            if matches!(self.peek(), Token::Comma) {
20963                self.advance();
20964                continue;
20965            }
20966            if matches!(self.peek(), Token::RParen) {
20967                self.advance();
20968                break;
20969            }
20970            return Err(self.err(alloc::format!(
20971                "expected ',' or ')' in {fn_name} column list, got {:?}",
20972                self.peek()
20973            )));
20974        }
20975        if coldefs.is_empty() {
20976            return Err(self.err(alloc::format!(
20977                "{fn_name} column-definition list must declare at least one column"
20978            )));
20979        }
20980        // Per column: (base ->> 'col')::type AS col. The base is the
20981        // per-element `value` column for the *set form, or the argument
20982        // itself for the scalar record form.
20983        let items: Vec<SelectItem> = coldefs
20984            .into_iter()
20985            .map(|(col, ty)| {
20986                let base = if is_set {
20987                    Expr::Column(ColumnName {
20988                        qualifier: None,
20989                        name: "value".to_string(),
20990                    })
20991                } else {
20992                    arg.clone()
20993                };
20994                SelectItem::Expr {
20995                    expr: Expr::Cast {
20996                        expr: Box::new(Expr::Binary {
20997                            lhs: Box::new(base),
20998                            op: BinOp::JsonGetText,
20999                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
21000                        }),
21001                        target: ty,
21002                    },
21003                    alias: Some(col),
21004                }
21005            })
21006            .collect();
21007        let from = if is_set {
21008            let elem_fn = if fn_name.starts_with("jsonb") {
21009                "jsonb_array_elements"
21010            } else {
21011                "json_array_elements"
21012            };
21013            Some(FromClause {
21014                primary: TableRef {
21015                    name: "value".to_string(),
21016                    alias: None,
21017                    only: false,
21018                    as_of_segment: None,
21019                    unnest_expr: Some(Box::new(Expr::FunctionCall {
21020                        name: elem_fn.to_string(),
21021                        args: alloc::vec![arg],
21022                    })),
21023                    unnest_column_aliases: alloc::vec!["value".to_string()],
21024                    with_ordinality: false,
21025                    generate_series_args: None,
21026                    lateral_subquery: None,
21027                    jsonb_each_text_arg: None,
21028                    table_fn_call: None,
21029                    rows_from: None,
21030                    json_table: None,
21031                    scalar_fn_item: false,
21032                },
21033                joins: Vec::new(),
21034            })
21035        } else {
21036            None
21037        };
21038        let inner = SelectStatement {
21039            locking: None,
21040            ctes: Vec::new(),
21041            distinct: false,
21042            distinct_on: Vec::new(),
21043            items,
21044            from,
21045            where_: None,
21046            group_by: None,
21047            group_by_all: false,
21048            having: None,
21049            unions: Vec::new(),
21050            order_by: Vec::new(),
21051            limit: None,
21052            offset: None,
21053            limit_with_ties: false,
21054            window_check_exprs: Vec::new(),
21055        };
21056        Ok(TableRef {
21057            name: alias.clone(),
21058            alias: Some(alias),
21059            only: false,
21060            as_of_segment: None,
21061            unnest_expr: None,
21062            unnest_column_aliases: Vec::new(),
21063            with_ordinality: false,
21064            generate_series_args: None,
21065            lateral_subquery: Some(Box::new(inner)),
21066            jsonb_each_text_arg: None,
21067            table_fn_call: None,
21068            rows_from: None,
21069            json_table: None,
21070            scalar_fn_item: false,
21071        })
21072    }
21073
21074    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
21075    /// Returns true when the clause was present. `WITH` alone (a
21076    /// CTE can never start here) is not enough — the ORDINALITY
21077    /// ident must follow, so a stray WITH still errors downstream.
21078    fn absorb_with_ordinality(&mut self) -> bool {
21079        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
21080            && matches!(self.tokens.get(self.pos + 1),
21081                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
21082        {
21083            self.advance();
21084            self.advance();
21085            true
21086        } else {
21087            false
21088        }
21089    }
21090
21091    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
21092    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
21093    /// Out-of-line: the caller sits on the FROM recursion chain.
21094    #[inline(never)]
21095    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
21096        let fn_name = match self.advance() {
21097            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
21098            _ => unreachable!("caller peeked an ident"),
21099        };
21100        self.advance(); // (
21101        let mut args: Vec<Expr> = Vec::new();
21102        if !matches!(self.peek(), Token::RParen) {
21103            loop {
21104                args.push(self.parse_expr(0)?);
21105                if matches!(self.peek(), Token::Comma) {
21106                    self.advance();
21107                    continue;
21108                }
21109                break;
21110            }
21111        }
21112        if !matches!(self.peek(), Token::RParen) {
21113            return Err(self.err(alloc::format!(
21114                "expected ')' after {fn_name}() arguments, got {:?}",
21115                self.peek()
21116            )));
21117        }
21118        self.advance();
21119        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
21120        // counter column rides after the function's own, and the alias list
21121        // names it.
21122        let with_ordinality = self.absorb_with_ordinality();
21123        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
21124        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
21125        Ok(TableRef {
21126            name,
21127            alias: alias_ident,
21128            only: false,
21129            as_of_segment: None,
21130            unnest_expr: None,
21131            unnest_column_aliases,
21132            with_ordinality,
21133            generate_series_args: None,
21134            lateral_subquery: None,
21135            jsonb_each_text_arg: None,
21136            table_fn_call: Some(Box::new((fn_name, args))),
21137            rows_from: None,
21138            json_table: None,
21139            scalar_fn_item: false,
21140        })
21141    }
21142
21143    /// v7.39 (round 205, JSON_TABLE) — parse
21144    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
21145    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
21146    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
21147    #[inline(never)]
21148    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
21149        self.advance(); // json_table
21150        self.advance(); // (
21151        let doc = Box::new(self.parse_expr(0)?);
21152        self.expect_comma_json_table()?;
21153        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
21154        // Optional `PASSING <expr> AS <name> [, …]`.
21155        let mut passing: Vec<(String, Expr)> = Vec::new();
21156        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
21157            self.advance();
21158            loop {
21159                let e = self.parse_expr(0)?;
21160                if !matches!(self.peek(), Token::As) {
21161                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
21162                }
21163                self.advance();
21164                let vname = match self.advance() {
21165                    Token::Ident(s) | Token::QuotedIdent(s) => s,
21166                    other => {
21167                        return Err(self.err(alloc::format!(
21168                            "expected PASSING variable name, got {other:?}"
21169                        )));
21170                    }
21171                };
21172                passing.push((vname, e));
21173                if matches!(self.peek(), Token::Comma) {
21174                    self.advance();
21175                    continue;
21176                }
21177                break;
21178            }
21179        }
21180        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21181            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
21182        }
21183        self.advance();
21184        let columns = self.parse_json_table_columns()?;
21185        if !matches!(self.peek(), Token::RParen) {
21186            return Err(self.err(alloc::format!(
21187                "expected ')' to close JSON_TABLE, got {:?}",
21188                self.peek()
21189            )));
21190        }
21191        self.advance();
21192        let alias_ident = self.parse_optional_alias()?;
21193        let name = alias_ident
21194            .clone()
21195            .unwrap_or_else(|| String::from("json_table"));
21196        Ok(TableRef {
21197            name,
21198            alias: alias_ident,
21199            only: false,
21200            as_of_segment: None,
21201            unnest_expr: None,
21202            unnest_column_aliases: Vec::new(),
21203            with_ordinality: false,
21204            generate_series_args: None,
21205            lateral_subquery: None,
21206            jsonb_each_text_arg: None,
21207            table_fn_call: None,
21208            rows_from: None,
21209            json_table: Some(Box::new(crate::ast::JsonTable {
21210                doc,
21211                row_path,
21212                columns,
21213                passing,
21214            })),
21215            scalar_fn_item: false,
21216        })
21217    }
21218
21219    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
21220        if !matches!(self.peek(), Token::Comma) {
21221            return Err(self.err(alloc::format!(
21222                "expected ',' after JSON_TABLE document, got {:?}",
21223                self.peek()
21224            )));
21225        }
21226        self.advance();
21227        Ok(())
21228    }
21229
21230    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
21231        match self.advance() {
21232            Token::String(s) => Ok(s),
21233            other => Err(self.err(alloc::format!(
21234                "expected {what} string literal, got {other:?}"
21235            ))),
21236        }
21237    }
21238
21239    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
21240    #[inline(never)]
21241    fn parse_json_table_columns(
21242        &mut self,
21243    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
21244        if !matches!(self.peek(), Token::LParen) {
21245            return Err(self.err("expected '(' after COLUMNS".into()));
21246        }
21247        self.advance();
21248        let mut cols = Vec::new();
21249        loop {
21250            cols.push(self.parse_json_table_one_column()?);
21251            if matches!(self.peek(), Token::Comma) {
21252                self.advance();
21253                continue;
21254            }
21255            break;
21256        }
21257        if !matches!(self.peek(), Token::RParen) {
21258            return Err(self.err(alloc::format!(
21259                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
21260                self.peek()
21261            )));
21262        }
21263        self.advance();
21264        Ok(cols)
21265    }
21266
21267    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
21268        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
21269        // NESTED [PATH] '<p>' COLUMNS (...)
21270        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
21271            self.advance();
21272            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21273                self.advance();
21274            }
21275            let path = self.parse_json_string_literal("NESTED PATH")?;
21276            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21277                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
21278            }
21279            self.advance();
21280            let columns = self.parse_json_table_columns()?;
21281            return Ok(JsonTableColumn::Nested { path, columns });
21282        }
21283        // <name> ...
21284        let name = match self.advance() {
21285            Token::Ident(s) | Token::QuotedIdent(s) => s,
21286            other => {
21287                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
21288            }
21289        };
21290        // <name> FOR ORDINALITY
21291        if matches!(self.peek(), Token::For) {
21292            self.advance();
21293            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
21294                return Err(self.err("expected ORDINALITY after FOR".into()));
21295            }
21296            self.advance();
21297            return Ok(JsonTableColumn::Ordinality { name });
21298        }
21299        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
21300        let ty = self.parse_column_type_name()?;
21301        let mut format_json = false;
21302        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21303            self.advance();
21304            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21305                return Err(self.err("expected JSON after FORMAT".into()));
21306            }
21307            self.advance();
21308            format_json = true;
21309        }
21310        let mut exists = false;
21311        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
21312            self.advance();
21313            exists = true;
21314        }
21315        let mut path = alloc::format!("$.{name}");
21316        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21317            self.advance();
21318            path = self.parse_json_string_literal("column PATH")?;
21319        }
21320        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21321            // `FORMAT JSON` after PATH (alternate placement).
21322            self.advance();
21323            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21324                self.advance();
21325            }
21326            format_json = true;
21327        }
21328        let mut wrapper = false;
21329        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
21330            self.advance();
21331            // optional CONDITIONAL/UNCONDITIONAL
21332            if matches!(self.peek(), Token::Ident(s)
21333                if s.eq_ignore_ascii_case("unconditional")
21334                    || s.eq_ignore_ascii_case("conditional"))
21335            {
21336                self.advance();
21337            }
21338            if !matches!(self.peek(), Token::Ident(s)
21339                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21340            {
21341                return Err(self.err("expected WRAPPER after WITH".into()));
21342            }
21343            self.advance();
21344            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
21345            if matches!(self.peek(), Token::Ident(s)
21346                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21347            {
21348                self.advance();
21349            }
21350            wrapper = true;
21351        }
21352        // ON EMPTY / ON ERROR clauses (two, in any order).
21353        let mut on_empty = JsonTableOnBehavior::Null;
21354        let mut on_error = JsonTableOnBehavior::Null;
21355        for _ in 0..2 {
21356            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
21357            {
21358                self.advance();
21359                Some(JsonTableOnBehavior::Error)
21360            } else if matches!(self.peek(), Token::Null) {
21361                self.advance();
21362                Some(JsonTableOnBehavior::Null)
21363            } else if matches!(self.peek(), Token::Default) {
21364                self.advance();
21365                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
21366            } else {
21367                None
21368            };
21369            let Some(behavior) = behavior else { break };
21370            // `ON {EMPTY|ERROR}`
21371            if !matches!(self.peek(), Token::On) {
21372                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
21373            }
21374            self.advance();
21375            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
21376                self.advance();
21377                on_empty = behavior;
21378            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
21379                self.advance();
21380                on_error = behavior;
21381            } else {
21382                return Err(self.err("expected EMPTY or ERROR after ON".into()));
21383            }
21384        }
21385        Ok(JsonTableColumn::Regular {
21386            name,
21387            ty,
21388            path,
21389            exists,
21390            format_json,
21391            wrapper,
21392            on_empty,
21393            on_error,
21394        })
21395    }
21396
21397    fn parse_optional_alias_with_columns(
21398        &mut self,
21399    ) -> Result<(Option<String>, Vec<String>), ParseError> {
21400        let alias = self.parse_optional_alias()?;
21401        if alias.is_none() {
21402            return Ok((None, Vec::new()));
21403        }
21404        let mut cols: Vec<String> = Vec::new();
21405        if matches!(self.peek(), Token::LParen) {
21406            self.advance();
21407            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21408                self.advance();
21409                cols.push(s);
21410                if matches!(self.peek(), Token::Comma) {
21411                    self.advance();
21412                    continue;
21413                }
21414                break;
21415            }
21416            if matches!(self.peek(), Token::RParen) {
21417                self.advance();
21418            }
21419        }
21420        Ok((alias, cols))
21421    }
21422
21423    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21424    /// whose keyword token was already consumed and whose `(` is the
21425    /// current token. Factored out of `parse_atom` (and marked
21426    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21427    /// recursive `parse_atom` frame — inlining them there enlarges the
21428    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21429    /// against, risking an overflow before the budget triggers.
21430    #[inline(never)]
21431    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21432        self.advance(); // (
21433        let mut args = Vec::new();
21434        if !matches!(self.peek(), Token::RParen) {
21435            loop {
21436                args.push(self.parse_expr(0)?);
21437                match self.peek() {
21438                    Token::Comma => {
21439                        self.advance();
21440                    }
21441                    Token::RParen => break,
21442                    other => {
21443                        return Err(self.err(alloc::format!(
21444                            "expected ',' or ')' in {name}() args, got {other:?}"
21445                        )));
21446                    }
21447                }
21448            }
21449        }
21450        self.advance(); // )
21451        Ok(Expr::FunctionCall {
21452            name: name.into(),
21453            args,
21454        })
21455    }
21456
21457    /// FROM-clause: a primary table reference plus zero-or-more joined
21458    /// peers expressed via either `, <table>` (cross-product, no ON) or
21459    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21460    /// v1.10 keeps the join list flat (left-associative nested-loop
21461    /// semantics).
21462    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21463        let primary = self.parse_table_ref()?;
21464        let primary_qual = primary
21465            .alias
21466            .clone()
21467            .unwrap_or_else(|| primary.name.clone());
21468        let joins = self.parse_from_joins(&primary_qual)?;
21469        Ok(FromClause { primary, joins })
21470    }
21471
21472    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21473    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21474    /// SAME grammar after its target table has already been consumed.
21475    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21476    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21477    /// be parsed forward, once.)
21478    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21479    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21480    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21481    /// desugaring, which needs a name for the left side of each equality.
21482    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21483        let mut joins = Vec::new();
21484        loop {
21485            // `, <table>` — cross-product with no ON.
21486            if matches!(self.peek(), Token::Comma) {
21487                self.advance();
21488                let table = self.parse_table_ref()?;
21489                joins.push(FromJoin {
21490                    kind: JoinKind::Cross,
21491                    table,
21492                    on: None,
21493                    using_cols: None,
21494                    natural: false,
21495                });
21496                continue;
21497            }
21498            // v7.37.16 — optional leading `NATURAL` before the join
21499            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21500            // not a lexer keyword (it arrives as a bare Ident), so match
21501            // it case-insensitively here. When present, no ON/USING
21502            // clause is allowed — the common columns are resolved at
21503            // execution time.
21504            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21505            if natural {
21506                self.advance();
21507            }
21508            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21509            // CROSS JOIN, and bare JOIN (defaults to INNER).
21510            let kind =
21511                match self.peek() {
21512                    Token::Inner => {
21513                        self.advance();
21514                        if !matches!(self.peek(), Token::Join) {
21515                            return Err(self
21516                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21517                        }
21518                        self.advance();
21519                        JoinKind::Inner
21520                    }
21521                    Token::Left => {
21522                        self.advance();
21523                        if matches!(self.peek(), Token::Outer) {
21524                            self.advance();
21525                        }
21526                        if !matches!(self.peek(), Token::Join) {
21527                            return Err(self.err(format!(
21528                                "expected JOIN after LEFT [OUTER], got {:?}",
21529                                self.peek()
21530                            )));
21531                        }
21532                        self.advance();
21533                        JoinKind::Left
21534                    }
21535                    Token::Cross => {
21536                        self.advance();
21537                        if !matches!(self.peek(), Token::Join) {
21538                            return Err(self
21539                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21540                        }
21541                        self.advance();
21542                        JoinKind::Cross
21543                    }
21544                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21545                    Token::Right => {
21546                        self.advance();
21547                        if matches!(self.peek(), Token::Outer) {
21548                            self.advance();
21549                        }
21550                        if !matches!(self.peek(), Token::Join) {
21551                            return Err(self.err(format!(
21552                                "expected JOIN after RIGHT [OUTER], got {:?}",
21553                                self.peek()
21554                            )));
21555                        }
21556                        self.advance();
21557                        JoinKind::Right
21558                    }
21559                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21560                    Token::Full => {
21561                        self.advance();
21562                        if matches!(self.peek(), Token::Outer) {
21563                            self.advance();
21564                        }
21565                        if !matches!(self.peek(), Token::Join) {
21566                            return Err(self.err(format!(
21567                                "expected JOIN after FULL [OUTER], got {:?}",
21568                                self.peek()
21569                            )));
21570                        }
21571                        self.advance();
21572                        JoinKind::FullOuter
21573                    }
21574                    Token::Join => {
21575                        self.advance();
21576                        JoinKind::Inner
21577                    }
21578                    _ => break,
21579                };
21580            let table = self.parse_table_ref()?;
21581            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21582            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21583            // where prev_table is the most-recent left-side table
21584            // (the previous join's table if any, else the FROM primary).
21585            // PG semantics around column merging are richer (USING'd
21586            // cols become deduplicated single output columns); for
21587            // sugar purposes the predicate-only form covers the
21588            // baseline corpus shape and chained `… JOIN x USING (k)
21589            // JOIN y USING (k)` calls.
21590            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21591            // common columns resolve at execution time.
21592            if natural {
21593                joins.push(FromJoin {
21594                    kind,
21595                    table,
21596                    on: None,
21597                    using_cols: None,
21598                    natural: true,
21599                });
21600                continue;
21601            }
21602            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21603            // v7.37.16 — capture the USING column list (in addition to
21604            // the ON desugar below) so the executor can perform PG's
21605            // column-merge on the output side.
21606            let mut using_cols: Option<Vec<String>> = None;
21607            let on = if matches!(self.peek(), Token::On) {
21608                self.advance();
21609                Some(self.parse_expr(0)?)
21610            } else if using_match {
21611                self.advance();
21612                if !matches!(self.peek(), Token::LParen) {
21613                    return Err(
21614                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21615                    );
21616                }
21617                self.advance();
21618                let mut cols: Vec<String> = Vec::new();
21619                loop {
21620                    match self.peek().clone() {
21621                        Token::Ident(s) | Token::QuotedIdent(s) => {
21622                            self.advance();
21623                            cols.push(s);
21624                        }
21625                        other => {
21626                            return Err(self.err(format!(
21627                                "expected column name inside USING (…), got {other:?}"
21628                            )));
21629                        }
21630                    }
21631                    match self.peek() {
21632                        Token::Comma => {
21633                            self.advance();
21634                            continue;
21635                        }
21636                        Token::RParen => {
21637                            self.advance();
21638                            break;
21639                        }
21640                        other => {
21641                            return Err(self.err(format!(
21642                                "expected ',' or ')' inside USING (…), got {other:?}"
21643                            )));
21644                        }
21645                    }
21646                }
21647                if cols.is_empty() {
21648                    return Err(self.err("USING (…) requires at least one column".to_string()));
21649                }
21650                using_cols = Some(cols.clone());
21651                // Pick the left-side alias: prev join's table if any,
21652                // else FROM primary. Use alias when present, else
21653                // table name (PG-equivalent qualifier).
21654                let left_qual: String = joins
21655                    .last()
21656                    .map(|j| {
21657                        j.table
21658                            .alias
21659                            .clone()
21660                            .unwrap_or_else(|| j.table.name.clone())
21661                    })
21662                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21663                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21664                let mut iter = cols.into_iter().map(|c| Expr::Binary {
21665                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21666                        qualifier: Some(left_qual.clone()),
21667                        name: c.clone(),
21668                    })),
21669                    op: crate::ast::BinOp::Eq,
21670                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21671                        qualifier: Some(right_qual.clone()),
21672                        name: c,
21673                    })),
21674                });
21675                let first = iter.next().expect("at least one col");
21676                Some(iter.fold(first, |acc, pred| Expr::Binary {
21677                    lhs: alloc::boxed::Box::new(acc),
21678                    op: crate::ast::BinOp::And,
21679                    rhs: alloc::boxed::Box::new(pred),
21680                }))
21681            } else if kind == JoinKind::Cross {
21682                None
21683            } else {
21684                return Err(self.err(format!(
21685                    "expected ON or USING after {:?} JOIN, got {:?}",
21686                    kind,
21687                    self.peek()
21688                )));
21689            };
21690            joins.push(FromJoin {
21691                kind,
21692                table,
21693                on,
21694                using_cols,
21695                natural: false,
21696            });
21697        }
21698        Ok(joins)
21699    }
21700
21701    /// Optional alias after an expression or table:
21702    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21703    /// accepted (PG-style implicit alias). Returns `None` if the next token
21704    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21705    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21706        if matches!(self.peek(), Token::As) {
21707            self.advance();
21708            // v7.39 (round 340, V56) — after AS the next token MUST be an
21709            // identifier. This used to return None and "let the caller
21710            // surface the error on the next expectation", but when AS is
21711            // the LAST token there is no next expectation: `SELECT 1 AS`
21712            // parsed clean and silently dropped the alias. PG rejects it.
21713            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21714                return self.expect_ident_like().map(Some);
21715            }
21716            return Err(self.err(alloc::format!(
21717                "expected an alias after AS, got {:?}",
21718                self.peek()
21719            )));
21720        }
21721        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21722        // grammar reserves a long list of follow-keywords from the
21723        // alias slot. SPG's bareword approximation: skip a small
21724        // set of idents that would otherwise be swallowed as the
21725        // table alias and break trailing clauses like CREATE
21726        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21727        // CONFLICT WHERE shapes.
21728        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21729            if is_alias_stopword(s) {
21730                return Ok(None);
21731            }
21732            return Ok(self.expect_ident_like().ok());
21733        }
21734        Ok(None)
21735    }
21736
21737    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21738    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21739        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21740        // error beats a stack overflow (an overflow aborts the
21741        // embedding host process).
21742        self.enter_nested()?;
21743        let r = self.parse_expr_inner(min_prec);
21744        self.nest_depth -= 1;
21745        r
21746    }
21747
21748    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21749    /// When the upcoming tokens form one, return the underlying
21750    /// operator token and the position just past the closing paren
21751    /// so the binary loop can dispatch on the plain operator.
21752    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21753        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21754            return None;
21755        }
21756        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21757            return None;
21758        }
21759        let mut i = self.pos + 2;
21760        // Optional schema qualifier (pg_catalog.<op> etc.).
21761        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21762            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21763        {
21764            i += 2;
21765        }
21766        let op_tok = self.tokens.get(i)?.clone();
21767        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21768            return None;
21769        }
21770        Some((i + 2, op_tok))
21771    }
21772
21773    /// PG operator symbols that lower onto function calls in
21774    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21775    /// family → regexp_like, comparison rung), `^@` (starts_with,
21776    /// comparison rung), `^` (power, tighter than `*`), `#`
21777    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21778    /// subset of the OR bits so the subtraction never borrows).
21779    fn try_symbol_operator(
21780        &mut self,
21781        lhs: &Expr,
21782        min_prec: u8,
21783    ) -> Result<Option<Expr>, ParseError> {
21784        enum Sym {
21785            Regex { ci: bool, negated: bool },
21786            Like { ci: bool, negated: bool },
21787            StartsWith,
21788            Power,
21789            Xor,
21790            RangeAdjacent,
21791        }
21792        // v7.39 (IS-precedence knife) — the low-precedence postfix
21793        // predicates ride this existing leaf call (zero new frame slots
21794        // on the nesting chain).
21795        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21796            return Ok(Some(e));
21797        }
21798        let (sym, prec): (Sym, u8) = match self.peek() {
21799            Token::Tilde => (
21800                Sym::Regex {
21801                    ci: false,
21802                    negated: false,
21803                },
21804                5,
21805            ),
21806            Token::TildeStar => (
21807                Sym::Regex {
21808                    ci: true,
21809                    negated: false,
21810                },
21811                5,
21812            ),
21813            Token::NotTilde => (
21814                Sym::Regex {
21815                    ci: false,
21816                    negated: true,
21817                },
21818                5,
21819            ),
21820            Token::NotTildeStar => (
21821                Sym::Regex {
21822                    ci: true,
21823                    negated: true,
21824                },
21825                5,
21826            ),
21827            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21828            Token::DoubleTilde => (
21829                Sym::Like {
21830                    ci: false,
21831                    negated: false,
21832                },
21833                5,
21834            ),
21835            Token::DoubleTildeStar => (
21836                Sym::Like {
21837                    ci: true,
21838                    negated: false,
21839                },
21840                5,
21841            ),
21842            Token::NotDoubleTilde => (
21843                Sym::Like {
21844                    ci: false,
21845                    negated: true,
21846                },
21847                5,
21848            ),
21849            Token::NotDoubleTildeStar => (
21850                Sym::Like {
21851                    ci: true,
21852                    negated: true,
21853                },
21854                5,
21855            ),
21856            Token::CaretAt => (Sym::StartsWith, 5),
21857            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21858            // tighter than `* / & |`, which the prec-9 rung preserves —
21859            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21860            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21861            Token::Caret => (Sym::Power, 9),
21862            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21863            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21864            Token::Hash => (Sym::Xor, 6),
21865            Token::Adjacent => (Sym::RangeAdjacent, 5),
21866            _ => return Ok(None),
21867        };
21868        if prec < min_prec {
21869            return Ok(None);
21870        }
21871        self.advance();
21872        let rhs = self.parse_expr(prec + 1)?;
21873        let out = match sym {
21874            Sym::Regex { ci, negated } => {
21875                let mut args = alloc::vec![lhs.clone(), rhs];
21876                if ci {
21877                    args.push(Expr::Literal(Literal::String(String::from("i"))));
21878                }
21879                maybe_not(
21880                    Expr::FunctionCall {
21881                        name: String::from("regexp_like"),
21882                        args,
21883                    },
21884                    negated,
21885                )
21886            }
21887            Sym::Like { ci, negated } => Expr::Like {
21888                expr: alloc::boxed::Box::new(lhs.clone()),
21889                pattern: alloc::boxed::Box::new(rhs),
21890                negated,
21891                case_insensitive: ci,
21892            },
21893            Sym::StartsWith => Expr::FunctionCall {
21894                name: String::from("starts_with"),
21895                args: alloc::vec![lhs.clone(), rhs],
21896            },
21897            Sym::Power => Expr::FunctionCall {
21898                name: String::from("power"),
21899                args: alloc::vec![lhs.clone(), rhs],
21900            },
21901            // `#` bitwise XOR — a real operator now (was desugared to
21902            // `(a|b)-(a&b)`, algebraically identical for integers but
21903            // undefined for bit strings; the direct op handles both).
21904            Sym::Xor => Expr::Binary {
21905                lhs: Box::new(lhs.clone()),
21906                op: BinOp::BitXor,
21907                rhs: Box::new(rhs),
21908            },
21909            // range `-|-` "is adjacent to" — lowered to a catalog function.
21910            Sym::RangeAdjacent => Expr::FunctionCall {
21911                name: String::from("range_adjacent"),
21912                args: alloc::vec![lhs.clone(), rhs],
21913            },
21914        };
21915        Ok(Some(out))
21916    }
21917
21918    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
21919    /// predicates, moved out of the tight postfix-cast loop: PG binds
21920    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
21921    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
21922    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
21923    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
21924    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
21925    /// when nothing at this position belongs to the family. Out-of-line
21926    /// (`inline(never)`): the caller sits on the per-nesting-level frame
21927    /// chain that MAX_NEST_DEPTH is tuned against.
21928    #[inline(never)]
21929    fn parse_postfix_predicate(
21930        &mut self,
21931        lhs: &Expr,
21932        min_prec: u8,
21933    ) -> Result<Option<Expr>, ParseError> {
21934        // Reached through try_symbol_operator (an existing leaf call of
21935        // the binary loop) so NO new stack slots land on the per-nesting
21936        // frame chain; the lhs clones only when a predicate actually
21937        // consumes it.
21938        match self.peek() {
21939            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
21940            // comparison family rung 5 (each +1 from the pre-XOR ladder).
21941            Token::Is if min_prec <= 4 => {}
21942            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
21943            Token::Not
21944                if min_prec <= 5
21945                    && matches!(
21946                        self.tokens.get(self.pos + 1),
21947                        Some(Token::Between | Token::In | Token::Like)
21948                    ) => {}
21949            Token::Not | Token::Ident(_)
21950                if min_prec <= 5
21951                    && (matches!(self.peek(), Token::Ident(s)
21952                            if s.eq_ignore_ascii_case("ilike")
21953                                || (self.mysql_dialect
21954                                    && (s.eq_ignore_ascii_case("regexp")
21955                                        || s.eq_ignore_ascii_case("rlike")))
21956                                || (s.eq_ignore_ascii_case("similar")
21957                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
21958                        || (matches!(self.peek(), Token::Not)
21959                            && matches!(self.tokens.get(self.pos + 1),
21960                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
21961                                    || (self.mysql_dialect
21962                                        && (s.eq_ignore_ascii_case("regexp")
21963                                            || s.eq_ignore_ascii_case("rlike")))
21964                                    || s.eq_ignore_ascii_case("similar")))) => {}
21965            _ => return Ok(None),
21966        }
21967        let mut expr = lhs.clone();
21968        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
21969        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
21970        if min_prec <= 4 {
21971            if matches!(self.peek(), Token::Is) {
21972                self.advance();
21973                let negated = if matches!(self.peek(), Token::Not) {
21974                    self.advance();
21975                    true
21976                } else {
21977                    false
21978                };
21979                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
21980                // mailrs pg_dump.
21981                if matches!(self.peek(), Token::Distinct) {
21982                    self.advance();
21983                    if !matches!(self.peek(), Token::From) {
21984                        return Err(self.err(format!(
21985                            "expected FROM after IS{} DISTINCT, got {:?}",
21986                            if negated { " NOT" } else { "" },
21987                            self.peek()
21988                        )));
21989                    }
21990                    self.advance();
21991                    // Right-hand side: parse at the same precedence
21992                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
21993                    // groups as `x IS DISTINCT FROM (a + b)`.
21994                    let rhs = self.parse_expr(5)?;
21995                    let op = if negated {
21996                        BinOp::IsNotDistinctFrom
21997                    } else {
21998                        BinOp::IsDistinctFrom
21999                    };
22000                    expr = Expr::Binary {
22001                        op,
22002                        lhs: Box::new(expr),
22003                        rhs: Box::new(rhs),
22004                    };
22005                    {
22006                        return Ok(Some(expr));
22007                    }
22008                }
22009                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
22010                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
22011                // Lowers onto pg_is_json(x, kind); NOT wraps the
22012                // call in a logical negation.
22013                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22014                if s.eq_ignore_ascii_case("json"))
22015                {
22016                    self.advance(); // JSON
22017                    let kind = match self.peek() {
22018                        Token::Ident(s) | Token::QuotedIdent(s)
22019                            if matches!(
22020                                s.to_ascii_lowercase().as_str(),
22021                                "value" | "object" | "array" | "scalar"
22022                            ) =>
22023                        {
22024                            let k = s.to_ascii_lowercase();
22025                            self.advance();
22026                            k
22027                        }
22028                        _ => "value".to_string(),
22029                    };
22030                    let call = Expr::FunctionCall {
22031                        name: "pg_is_json".to_string(),
22032                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
22033                    };
22034                    expr = if negated {
22035                        Expr::Unary {
22036                            op: UnOp::Not,
22037                            expr: Box::new(call),
22038                        }
22039                    } else {
22040                        call
22041                    };
22042                    {
22043                        return Ok(Some(expr));
22044                    }
22045                }
22046                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
22047                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
22048                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
22049                {
22050                    let form_kw = match self.peek() {
22051                        Token::Ident(s) | Token::QuotedIdent(s)
22052                            if matches!(
22053                                s.to_ascii_uppercase().as_str(),
22054                                "NFC" | "NFD" | "NFKC" | "NFKD"
22055                            ) && matches!(
22056                                self.tokens.get(self.pos + 1),
22057                                Some(Token::Ident(n) | Token::QuotedIdent(n))
22058                                    if n.eq_ignore_ascii_case("normalized")
22059                            ) =>
22060                        {
22061                            Some(s.to_ascii_uppercase())
22062                        }
22063                        _ => None,
22064                    };
22065                    let bare_normalized = form_kw.is_none()
22066                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22067                        if s.eq_ignore_ascii_case("normalized"));
22068                    if form_kw.is_some() || bare_normalized {
22069                        if form_kw.is_some() {
22070                            self.advance(); // form keyword
22071                        }
22072                        self.advance(); // NORMALIZED
22073                        let mut args = alloc::vec![expr];
22074                        if let Some(f) = form_kw {
22075                            args.push(Expr::Literal(Literal::String(f)));
22076                        }
22077                        let call = Expr::FunctionCall {
22078                            name: "is_normalized".to_string(),
22079                            args,
22080                        };
22081                        expr = if negated {
22082                            Expr::Unary {
22083                                op: UnOp::Not,
22084                                expr: Box::new(call),
22085                            }
22086                        } else {
22087                            call
22088                        };
22089                        {
22090                            return Ok(Some(expr));
22091                        }
22092                    }
22093                }
22094                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
22095                // three-valued boolean tests. IS TRUE/FALSE never
22096                // return NULL, so they lower to CASE forms whose
22097                // ELSE catches the NULL branch; IS UNKNOWN on a
22098                // boolean is exactly IS NULL.
22099                if matches!(self.peek(), Token::True | Token::False)
22100                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
22101                {
22102                    let tok = self.advance();
22103                    let test = match tok {
22104                        Token::True => Some(true),
22105                        Token::False => Some(false),
22106                        _ => None, // UNKNOWN
22107                    };
22108                    // v7.39 (round 328, V45) — kept as what the user
22109                    // wrote. These used to be lowered here into `CASE` /
22110                    // `IS NULL`; the semantics were right but the AST no
22111                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
22112                    // was echoed back as
22113                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
22114                    expr = Expr::BoolTest {
22115                        expr: Box::new(expr),
22116                        value: test,
22117                        negated,
22118                    };
22119                    {
22120                        return Ok(Some(expr));
22121                    }
22122                }
22123                if !matches!(self.peek(), Token::Null) {
22124                    return Err(self.err(format!(
22125                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
22126                    if negated { " NOT" } else { "" },
22127                    self.peek()
22128                )));
22129                }
22130                self.advance();
22131                expr = Expr::IsNull {
22132                    expr: Box::new(expr),
22133                    negated,
22134                };
22135                {
22136                    return Ok(Some(expr));
22137                }
22138            }
22139        }
22140        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
22141        if min_prec <= 5 {
22142            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
22143            // Look one token ahead so a stray `NOT` not followed by any of
22144            // these flows through to the early return below untouched.
22145            let negated = if matches!(self.peek(), Token::Not) {
22146                let next = self.tokens.get(self.pos + 1);
22147                matches!(next, Some(Token::Between | Token::In | Token::Like))
22148                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22149                    || (self.mysql_dialect
22150                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
22151                    || s.eq_ignore_ascii_case("similar"))
22152            } else {
22153                false
22154            };
22155            if negated {
22156                self.advance();
22157            }
22158            if matches!(self.peek(), Token::Between) {
22159                expr = self.parse_between_tail(expr, negated)?;
22160                {
22161                    return Ok(Some(expr));
22162                }
22163            }
22164            if matches!(self.peek(), Token::In) {
22165                if self.suppress_in_tail && !negated {
22166                    // POSITION(sub IN str) — IN belongs to the
22167                    // enclosing function syntax; stop here.
22168                    {
22169                        return Ok(None);
22170                    }
22171                }
22172                expr = self.parse_in_tail(expr, negated)?;
22173                {
22174                    return Ok(Some(expr));
22175                }
22176            }
22177            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
22178            // lowers onto the internal __similar_to(expr, pat[, esc]) call
22179            // (the SQL→regex transform runs inside, in the backtracking-
22180            // friendly shape SPG's matcher needs).
22181            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
22182                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
22183            {
22184                self.advance(); // SIMILAR
22185                self.advance(); // TO
22186                let pattern = self.parse_expr(6)?;
22187                let mut args = alloc::vec![expr, pattern];
22188                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22189                    self.advance();
22190                    args.push(self.parse_expr(6)?);
22191                }
22192                let call = Expr::FunctionCall {
22193                    name: "__similar_to".to_string(),
22194                    args,
22195                };
22196                expr = maybe_not(call, negated);
22197                {
22198                    return Ok(Some(expr));
22199                }
22200            }
22201            if matches!(self.peek(), Token::Like) {
22202                self.advance();
22203                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
22204                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
22205                    expr = q;
22206                    {
22207                        return Ok(Some(expr));
22208                    }
22209                }
22210                // Pattern at the same precedence as other comparison RHSes —
22211                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
22212                let mut pattern = self.parse_expr(6)?;
22213                // `ESCAPE 'c'` — rewrite a literal pattern to the
22214                // default backslash escape at parse time. Custom
22215                // escapes on non-literal patterns would need
22216                // matcher support; error honestly.
22217                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22218                    self.advance();
22219                    let esc = self.parse_expr(6)?;
22220                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
22221                }
22222                expr = Expr::Like {
22223                    expr: Box::new(expr),
22224                    pattern: Box::new(pattern),
22225                    negated,
22226                    case_insensitive: false,
22227                };
22228                {
22229                    return Ok(Some(expr));
22230                }
22231            }
22232            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
22233            // keyword reaches us as a plain identifier.
22234            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
22235                self.advance();
22236                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
22237                    expr = q;
22238                    {
22239                        return Ok(Some(expr));
22240                    }
22241                }
22242                let pattern = self.parse_expr(6)?;
22243                expr = Expr::Like {
22244                    expr: Box::new(expr),
22245                    pattern: Box::new(pattern),
22246                    negated,
22247                    case_insensitive: true,
22248                };
22249                {
22250                    return Ok(Some(expr));
22251                }
22252            }
22253            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
22254            // operator (RLIKE is the alias). It is a keyword, not `~`, and
22255            // matches case-insensitively under the default collation, so it
22256            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
22257            // `~*` operator uses, wrapped in NOT when negated.
22258            if self.mysql_dialect
22259                && matches!(self.peek(), Token::Ident(s)
22260                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
22261            {
22262                self.advance();
22263                let pattern = self.parse_expr(6)?;
22264                let call = Expr::FunctionCall {
22265                    name: String::from("regexp_like"),
22266                    args: alloc::vec![
22267                        expr,
22268                        pattern,
22269                        Expr::Literal(Literal::String(String::from("i"))),
22270                    ],
22271                };
22272                return Ok(Some(maybe_not(call, negated)));
22273            }
22274        }
22275        let _ = expr;
22276        Ok(None)
22277    }
22278
22279    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
22280        let mut lhs = self.parse_unary()?;
22281        let mut chain_len = 0usize;
22282        loop {
22283            // OPERATOR([schema.]op) reduces to its underlying
22284            // operator token before the normal dispatch.
22285            let explicit = self.peek_explicit_operator();
22286            let dispatch = match &explicit {
22287                Some((_, tok)) => self.binop_here(tok),
22288                None => self.binop_here(self.peek()),
22289            };
22290            let Some((op, prec)) = dispatch else {
22291                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
22292                // of the symbol family. `binop_here` answers None for them
22293                // because they lower onto function calls rather than a
22294                // BinOp, and the fallback below reads `self.peek()` — the
22295                // word OPERATOR, not the operator. `pg_dump` writes every
22296                // catalog predicate this way, so its first query failed
22297                // and no dump ran:
22298                //
22299                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
22300                //
22301                // Collapsing the wrapper to the operator it names puts the
22302                // token where the fallback already looks.
22303                if let Some((next, op_tok)) = explicit {
22304                    self.tokens.splice(self.pos..next, [op_tok]);
22305                }
22306                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
22307                    lhs = e;
22308                    chain_len += 1;
22309                    if chain_len > MAX_BINARY_CHAIN {
22310                        return Err(self.err(alloc::format!(
22311                            "more than {MAX_BINARY_CHAIN} chained binary operators"
22312                        )));
22313                    }
22314                    continue;
22315                }
22316                break;
22317            };
22318            if prec < min_prec {
22319                break;
22320            }
22321            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
22322            // iteratively but evaluates and drops recursively;
22323            // depth beyond the budget overflows worker stacks.
22324            chain_len += 1;
22325            if chain_len > MAX_BINARY_CHAIN {
22326                return Err(self.err(alloc::format!(
22327                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
22328                )));
22329            }
22330            match explicit {
22331                Some((end_pos, _)) => self.pos = end_pos,
22332                None => {
22333                    self.advance();
22334                }
22335            }
22336            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
22337            // ANY is a bare ident; ALL is a reserved Token. Both
22338            // require an immediate `(` to disambiguate from
22339            // identifier columns named `any` / `all`.
22340            let any_kind = match self.peek() {
22341                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
22342                    Some(false)
22343                }
22344                Token::Ident(s) | Token::QuotedIdent(s)
22345                    if (s.eq_ignore_ascii_case("any")
22346                        || s.eq_ignore_ascii_case("some")
22347                        || s.eq_ignore_ascii_case("all"))
22348                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22349                {
22350                    Some(!s.eq_ignore_ascii_case("all"))
22351                }
22352                _ => None,
22353            };
22354            if let Some(is_any) = any_kind {
22355                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
22356                continue;
22357            }
22358            let rhs = self.parse_expr(prec + 1)?;
22359            lhs = Expr::Binary {
22360                lhs: Box::new(lhs),
22361                op,
22362                rhs: Box::new(rhs),
22363            };
22364        }
22365        Ok(lhs)
22366    }
22367
22368    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
22369    /// and the array form.
22370    ///
22371    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
22372    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
22373    /// this block's `Expr` temporaries and four `format!` sites slots in
22374    /// that frame on every level of `((((1))))`, which never reaches it.
22375    #[inline(never)]
22376    fn parse_any_all_rhs(
22377        &mut self,
22378        lhs: Expr,
22379        op: BinOp,
22380        is_any: bool,
22381    ) -> Result<Expr, ParseError> {
22382        self.advance(); // ident
22383        self.advance(); // (
22384        // `x op ANY (SELECT …)` — the quantified-subquery
22385        // form. `= ANY` is exactly IN; the other operators
22386        // lower onto EXISTS over the subquery as a derived
22387        // table, comparing against its single projection
22388        // aliased __v (x's columns resolve correlated).
22389        // ALL is the negated-EXISTS complement; a NULL
22390        // element makes PG return NULL where this lowering
22391        // returns true — the NOT NULL column case (the
22392        // practical one) is exact.
22393        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
22394            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22395            // legal PG too (round-151 sibling). Out-of-line
22396            // (#[inline(never)] helper) — this sits on
22397            // parse_expr's recursive frame and the two-armed
22398            // SELECT temporary blew the nesting-budget stack.
22399            let mut sub = self.parse_any_all_select_body()?;
22400            if !matches!(self.peek(), Token::RParen) {
22401                return Err(self.err(alloc::format!(
22402                    "expected ')' after ANY/ALL subquery, got {:?}",
22403                    self.peek()
22404                )));
22405            }
22406            self.advance();
22407            if sub.items.len() != 1 {
22408                return Err(self.err(alloc::format!(
22409                    "ANY/ALL subquery must return one column, got {}",
22410                    sub.items.len()
22411                )));
22412            }
22413            if is_any && matches!(op, BinOp::Eq) {
22414                return Ok(Expr::InSubquery {
22415                    expr: Box::new(lhs),
22416                    subquery: Box::new(sub),
22417                    negated: false,
22418                });
22419            }
22420            // The engine's subquery resolvers materialise
22421            // the single-column result into an ARRAY the
22422            // existing AnyAll three-valued eval consumes.
22423            return Ok(Expr::AnyAll {
22424                expr: Box::new(lhs),
22425                op,
22426                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22427                is_any,
22428            });
22429        }
22430        let arr = self.parse_expr(0)?;
22431        if !matches!(self.peek(), Token::RParen) {
22432            return Err(self.err(alloc::format!(
22433                "expected ')' after ANY/ALL argument, got {:?}",
22434                self.peek()
22435            )));
22436        }
22437        self.advance();
22438        Ok(Expr::AnyAll {
22439            expr: Box::new(lhs),
22440            op,
22441            array: Box::new(arr),
22442            is_any,
22443        })
22444    }
22445
22446    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22447    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22448    #[inline(never)]
22449    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22450        self.advance();
22451        let e = self.parse_expr(9)?;
22452        Ok(build_center_call(e))
22453    }
22454
22455    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22456    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22457    /// unary minus.
22458    ///
22459    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22460    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22461    /// the Expr-sized local stays out of that frame.
22462    #[inline(never)]
22463    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22464        self.advance();
22465        let e = self.parse_expr(9)?;
22466        Ok(Expr::FunctionCall {
22467            name: alloc::string::String::from(name),
22468            args: alloc::vec![e],
22469        })
22470    }
22471
22472    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22473    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22474    #[inline(never)]
22475    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22476        self.advance();
22477        let e = self.parse_expr(9)?;
22478        Ok(Expr::FunctionCall {
22479            name: alloc::string::String::from(if vertical {
22480                "isvertical"
22481            } else {
22482                "ishorizontal"
22483            }),
22484            args: alloc::vec![e],
22485        })
22486    }
22487
22488    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22489    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22490    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22491    #[inline(never)]
22492    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22493        self.advance();
22494        let e = self.parse_expr(9)?;
22495        Ok(Expr::Cast {
22496            expr: Box::new(e),
22497            target: CastTarget::Named("binary".to_string()),
22498        })
22499    }
22500
22501    /// The prefix operators that share one shape: take the token, parse
22502    /// an operand at `prec`, wrap it.
22503    ///
22504    /// `#[inline(never)]`, and one function instead of five arms, for the
22505    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22506    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22507    /// debug build gives EVERY arm's locals a slot in the frame, whichever
22508    /// arm runs. `((((1))))` reaches none of these arms and was carrying
22509    /// five `Expr`-sized locals per level for them anyway.
22510    #[inline(never)]
22511    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22512        self.advance();
22513        let e = self.parse_expr(prec)?;
22514        Ok(Expr::Unary {
22515            op,
22516            expr: Box::new(e),
22517        })
22518    }
22519
22520    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22521    /// and separate from it because of the literal folding below and the
22522    /// `format!` temporaries that folding needs.
22523    #[inline(never)]
22524    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22525        self.advance();
22526        // v7.39 (round 549) — fold the sign into an integer literal that
22527        // only fits once it is negative.
22528        //
22529        // `9223372036854775808` is one past i64::MAX, so the lexer hands
22530        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22531        // folds the sign first, so `-9223372036854775808` is a bigint
22532        // there — and `-9223372036854775808 - 1` raises "bigint out of
22533        // range" where SPG quietly answered -9223372036854775809, a value
22534        // no bigint can hold. The arithmetic itself was already checked;
22535        // only the literal's type was wrong.
22536        if let Token::Numeric(lit) = self.peek()
22537            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22538        {
22539            self.advance();
22540            return Ok(Expr::Literal(Literal::Integer(folded)));
22541        }
22542        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22543        // `<->` slotted into 5 and arithmetic shifted up).
22544        let e = self.parse_expr(9)?;
22545        Ok(Expr::Unary {
22546            op: UnOp::Neg,
22547            expr: Box::new(e),
22548        })
22549    }
22550
22551    /// tsquery `!!` prefix negation, lowered to the catalog function.
22552    /// Binds like unary minus. Out-of-line for the frame reason on
22553    /// `parse_unary_op`.
22554    #[inline(never)]
22555    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22556        self.advance();
22557        let e = self.parse_expr(9)?;
22558        Ok(Expr::FunctionCall {
22559            name: String::from("tsquery_not"),
22560            args: alloc::vec![e],
22561        })
22562    }
22563
22564    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22565        match self.peek() {
22566            // NOT binds tighter than AND / XOR / OR but looser than
22567            // comparisons — its operand takes everything ≥ the comparison
22568            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22569            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22570            // was rung 3, behaviour-identical when 3 was unused; AND now
22571            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22572            Token::Not => self.parse_unary_op(UnOp::Not, 4),
22573            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22574            // The body is out-of-line: `parse_unary` is one of the three
22575            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22576            // inline arm here overflowed the native stack in
22577            // `nesting_budget_errors_cleanly` — the guard test caught it,
22578            // exactly as the eval-side cliff did in rounds 346 and 351.
22579            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22580                self.parse_binary_prefix()
22581            }
22582            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22583            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22584            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22585            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22586            Token::Minus => self.parse_prefix_minus(),
22587            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22588            // worked only because the lexer reads it as one signed literal;
22589            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22590            // PG18 and MariaDB take all of them. Binds like unary minus.
22591            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22592            // Bitwise NOT binds like unary minus.
22593            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22594            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22595            // "center of" operator; desugars to center(x). The whole arm
22596            // is out-of-line: parse_unary sits on the per-nesting-level
22597            // frame chain that MAX_NEST_DEPTH is tuned against, so no
22598            // Expr-sized local may live in this frame.
22599            Token::TsMatch => self.parse_prefix_center(),
22600            // v7.39 (round 508) — the prefix operators that are named
22601            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22602            // is length. Out-of-line for the same nesting-frame reason as
22603            // parse_prefix_center — parse_unary sits on the recursive cycle
22604            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22605            // live in this frame.
22606            Token::At => self.parse_prefix_call("abs"),
22607            Token::Hash => self.parse_prefix_call("npoints"),
22608            Token::AtMinusAt => self.parse_prefix_call("length"),
22609            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22610            // "is horizontal" (lseg / line); desugars to the existing
22611            // isvertical()/ishorizontal() functions. Out-of-line for the
22612            // same nesting-frame reason as parse_prefix_center.
22613            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22614            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22615            Token::DoubleBang => self.parse_prefix_tsquery_not(),
22616            _ => self.parse_atom(),
22617        }
22618    }
22619
22620    /// Parse a parenthesised scalar subquery body after the caller has consumed
22621    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22622    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22623    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22624    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22625    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22626    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22627    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22628    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22629    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22630    /// tips the deep-nesting test into a stack overflow).
22631    #[inline(never)]
22632    fn array_subquery_ahead(&self) -> bool {
22633        if !matches!(self.peek(), Token::LParen) {
22634            return false;
22635        }
22636        matches!(
22637            self.tokens.get(self.pos + 1),
22638            Some(Token::Select | Token::Values)
22639        ) || matches!(
22640            self.tokens.get(self.pos + 1),
22641            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22642        )
22643    }
22644
22645    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22646    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22647    /// locals stay off parse_atom's recursive frame (round 105).
22648    #[inline(never)]
22649    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22650        self.advance(); // consume `[`
22651        let mut items: Vec<Expr> = Vec::new();
22652        if !matches!(self.peek(), Token::RBracket) {
22653            loop {
22654                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22655                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22656                if matches!(self.peek(), Token::LBracket) {
22657                    items.push(self.parse_array_bracket_body()?);
22658                } else {
22659                    items.push(self.parse_expr(0)?);
22660                }
22661                match self.peek() {
22662                    Token::Comma => {
22663                        self.advance();
22664                    }
22665                    Token::RBracket => break,
22666                    other => {
22667                        return Err(self.err(alloc::format!(
22668                            "expected ',' or ']' in ARRAY literal, got {other:?}"
22669                        )));
22670                    }
22671                }
22672            }
22673        }
22674        self.advance(); // consume `]`
22675        Ok(Expr::Array(items))
22676    }
22677
22678    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22679    /// is already consumed; the current token is `(`. Desugars to a scalar
22680    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22681    /// the subquery's single-column rows in order — reusing the existing
22682    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22683    /// keeps the large `Statement` local off parse_atom's recursive frame.
22684    #[inline(never)]
22685    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22686        self.advance(); // consume `(`
22687        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22688            if w.eq_ignore_ascii_case("with"));
22689        let sub = if is_with {
22690            self.advance(); // WITH
22691            self.parse_with_cte_then_select()?
22692        } else {
22693            self.parse_select_stmt()?
22694        };
22695        if !matches!(self.peek(), Token::RParen) {
22696            return Err(self.err(alloc::format!(
22697                "expected ')' to close ARRAY(subquery), got {:?}",
22698                self.peek()
22699            )));
22700        }
22701        self.advance(); // consume `)`
22702        // Reuse the parser to build the array_agg wrapper from the subquery's
22703        // canonical text — avoids hand-constructing the derived-table AST.
22704        let wrapper = alloc::format!(
22705            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22706        );
22707        let stmt = parse_statement(&wrapper)
22708            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22709        let Statement::Select(sel) = stmt else {
22710            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22711        };
22712        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22713    }
22714
22715    #[inline(never)]
22716    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22717        let inner = if is_with {
22718            self.advance(); // WITH
22719            self.parse_with_cte_then_select()?
22720        } else {
22721            self.parse_select_stmt()?
22722        };
22723        match self.advance() {
22724            Token::RParen => {
22725                let Statement::Select(s) = inner else {
22726                    return Err(ParseError {
22727                        message: "scalar subquery body must be a SELECT".into(),
22728                        token_pos: self.consumed_pos(),
22729                    });
22730                };
22731                Ok(Expr::ScalarSubquery(Box::new(s)))
22732            }
22733            other => Err(ParseError {
22734                message: format!("expected ')' after scalar subquery, got {other:?}"),
22735                token_pos: self.consumed_pos(),
22736            }),
22737        }
22738    }
22739
22740    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22741    /// literals. The lexer splits them into an ident + string; recombine
22742    /// here. Out-of-line and returning `Option` so `parse_atom` — the
22743    /// recursive frame the 768 KiB stack budget is tuned against — pays no
22744    /// frame for the `body` / `bits` strings and their char loops (the
22745    /// round-367 frame cliff, M20).
22746    #[inline(never)]
22747    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22748        let is_hex = match self.peek() {
22749            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22750            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22751            _ => return None,
22752        };
22753        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22754            return None;
22755        }
22756        // v7.39.3 — where the LITERAL starts, because the errors below
22757        // are about the literal and both engines point at it. `err`
22758        // reports the CURRENT token, which by then is the one after the
22759        // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22760        // `near '…'` snippet — which runs from the reported position to
22761        // the end — came out empty where MySQL 9.7.2 says `near
22762        // 'x'123''`.
22763        let lit_pos = self.pos;
22764        self.advance();
22765        let Token::String(body) = self.advance() else {
22766            unreachable!("guarded above");
22767        };
22768        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22769        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22770        // (hex pairs, even count required — MariaDB errors on an odd
22771        // count); `b'1010'` packs its bits big-endian, left-padded to a
22772        // byte. Lower both onto the bytea cast.
22773        if self.mysql_dialect {
22774            if is_hex {
22775                if body.len() % 2 == 1 {
22776                    return Some(Err(self.err_at(
22777                        lit_pos,
22778                        alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22779                    )));
22780                }
22781                for c in body.chars() {
22782                    if !c.is_ascii_hexdigit() {
22783                        return Some(Err(self.err_at(
22784                            lit_pos,
22785                            alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22786                        )));
22787                    }
22788                }
22789                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22790            }
22791            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22792                return Some(Err(self.err_at(
22793                    lit_pos,
22794                    alloc::format!("invalid binary digit {bad:?} in b'…'"),
22795                )));
22796            }
22797            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22798        }
22799        let bits = if is_hex {
22800            let mut out = String::with_capacity(body.len() * 4);
22801            for c in body.chars() {
22802                let Some(d) = c.to_digit(16) else {
22803                    // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22804                    // own quoting: `"g" is not a valid hexadecimal
22805                    // digit` (measured, with the caret on the literal).
22806                    return Some(Err(self.err_at(
22807                        lit_pos,
22808                        alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22809                    )));
22810                };
22811                out.push_str(&alloc::format!("{d:04b}"));
22812            }
22813            out
22814        } else {
22815            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22816                return Some(Err(self.err_at(
22817                    lit_pos,
22818                    alloc::format!("\"{bad}\" is not a valid binary digit"),
22819                )));
22820            }
22821            body
22822        };
22823        // Route through the postfix-cast loop so a chained cast like
22824        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22825        // of erroring at the `::`.
22826        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22827        // literal keeps its exact length, while an explicit `::bit` cast is
22828        // bit(1) with pad/truncate semantics (PG).
22829        Some(self.finish_postfix_casts(Expr::Cast {
22830            expr: Box::new(Expr::Literal(Literal::String(bits))),
22831            target: CastTarget::Named("__bit_literal".to_string()),
22832        }))
22833    }
22834
22835    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22836        if let Some(res) = self.try_parse_bit_string_literal() {
22837            return res;
22838        }
22839        let tok_pos = self.pos;
22840        match self.advance() {
22841            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22842            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22843            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22844            // carrying the source mantissa + scale so no precision is lost. A
22845            // literal too wide for i128 falls back to double precision.
22846            // Out-of-line (#[inline(never)]) — this arm sits on the
22847            // parse_expr recursion chain; its expansion locals must not
22848            // widen the recursive frame (debug frame-cliff discipline).
22849            Token::Numeric(s) => match numeric_token_to_literal(s) {
22850                Ok(lit) => Ok(Expr::Literal(lit)),
22851                Err(msg) => Err(self.err(msg)),
22852            },
22853            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22854            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22855            // (the lexer only emits this token in the MySQL dialect). Lower
22856            // onto the existing bytea cast; out-of-line to keep this arm off
22857            // the parse recursion frame.
22858            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22859            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22860            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22861            Token::Null => Ok(Expr::Literal(Literal::Null)),
22862            // v6.1.1 — `$N` placeholder. The actual Value lookup
22863            // happens in the engine eval path against the prepared-
22864            // statement bind buffer.
22865            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22866            Token::LParen => {
22867                // v4.10: `(SELECT ...)` in expression position is a
22868                // scalar subquery; otherwise it's a parenthesised
22869                // expression. Peek for SELECT keyword to dispatch.
22870                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22871                // lexes as Ident("with") (not a reserved token). The subquery body
22872                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22873                // so its large `Statement` local stays out of parse_atom's stack
22874                // frame — parse_atom is on the recursive `((…))` cycle and the
22875                // nesting budget is tuned to its frame size).
22876                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22877                    if s.eq_ignore_ascii_case("with"));
22878                if matches!(self.peek(), Token::Select) || is_with {
22879                    self.parse_paren_scalar_subquery(is_with)
22880                } else {
22881                    let e = self.parse_expr(0)?;
22882                    // `(a, b, …)` — a row constructor. Valid only
22883                    // in front of a comparison operator or [NOT]
22884                    // IN; both expand at parse time (lexicographic
22885                    // comparison / OR'd row equalities).
22886                    if matches!(self.peek(), Token::Comma) {
22887                        let mut row = alloc::vec![e];
22888                        while matches!(self.peek(), Token::Comma) {
22889                            self.advance();
22890                            row.push(self.parse_expr(0)?);
22891                        }
22892                        if !matches!(self.peek(), Token::RParen) {
22893                            return Err(self.err(alloc::format!(
22894                                "expected ')' after row constructor, got {:?}",
22895                                self.peek()
22896                            )));
22897                        }
22898                        self.advance();
22899                        // A bare `(a, b, …)` row constructor can carry postfix
22900                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
22901                        // early return here skips parse_atom's tail postfix
22902                        // pass, so fold casts in explicitly. For the
22903                        // comparison / predicate forms nothing postfix follows,
22904                        // so this is a no-op.
22905                        return self
22906                            .parse_row_comparison_tail(row)
22907                            .and_then(|e| self.finish_postfix_casts(e));
22908                    }
22909                    match self.advance() {
22910                        Token::RParen => Ok(e),
22911                        other => Err(ParseError {
22912                            message: format!("expected ')', got {other:?}"),
22913                            token_pos: self.consumed_pos(),
22914                        }),
22915                    }
22916                }
22917            }
22918            Token::LBracket => self.parse_vector_literal_body(),
22919            Token::Extract => self.parse_extract_atom(),
22920            Token::Interval => self.parse_interval_atom(),
22921            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
22922            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
22923            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
22924            // expression position calling the PG `left(string, n)` /
22925            // `right(string, n)` function; rebuild the AST as a regular
22926            // function call so the engine's apply_function dispatch picks
22927            // it up. Delegated to a #[inline(never)] helper so its locals
22928            // don't bloat this recursive `parse_atom` frame (the nesting
22929            // budget in `enter_nested` is tuned to parse_atom's size).
22930            Token::Left if matches!(self.peek(), Token::LParen) => {
22931                self.parse_lr_string_function_call("left")
22932            }
22933            Token::Right if matches!(self.peek(), Token::LParen) => {
22934                self.parse_lr_string_function_call("right")
22935            }
22936            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
22937            // token; we match on the bare ident. NOT is a token
22938            // (consumed in the comparison rung), but `EXISTS (...)`
22939            // at the top of an expression starts here.
22940            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
22941                self.parse_exists_atom(false)
22942            }
22943            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
22944            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
22945            // CASE is a bare ident; we dispatch on lowercase match.
22946            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
22947                self.parse_case_atom()
22948            }
22949            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
22950            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
22951            // '…'`. Lower onto the ::cast node so the existing
22952            // runtime text→date/timestamp paths do the parsing. The
22953            // string must follow immediately, else the ident stays a
22954            // plain column reference.
22955            Token::Ident(s)
22956                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
22957                    && matches!(self.peek(), Token::String(_)) =>
22958            {
22959                let target =
22960                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
22961                let Token::String(lit) = self.advance() else {
22962                    unreachable!("peek guaranteed a string token");
22963                };
22964                Ok(Expr::Cast {
22965                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22966                    target,
22967                })
22968            }
22969            // v7.39 (round 221) — the SQL-standard long spellings:
22970            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
22971            // TIME ZONE '…'`. Consume the modifier and lower to the same
22972            // typed-literal cast (`timetz` / `timestamptz` for WITH).
22973            Token::Ident(s)
22974                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
22975                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
22976                        || w.eq_ignore_ascii_case("without"))
22977                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
22978                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
22979                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
22980            {
22981                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
22982                self.advance(); // WITH / WITHOUT
22983                self.advance(); // TIME
22984                self.advance(); // ZONE
22985                let Token::String(lit) = self.advance() else {
22986                    unreachable!("guard checked a string token");
22987                };
22988                let base = s.to_ascii_lowercase();
22989                let target = match (base.as_str(), with_tz) {
22990                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
22991                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
22992                    (_, true) => CastTarget::Timestamptz,
22993                    (_, false) => CastTarget::Timestamp,
22994                };
22995                Ok(Expr::Cast {
22996                    expr: Box::new(Expr::Literal(Literal::String(lit))),
22997                    target,
22998                })
22999            }
23000            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
23001            // gathers the subquery's single-column rows (in its row order)
23002            // into an array. Desugared to `array_agg` over the subquery as a
23003            // derived table; out-of-line to keep parse_atom's frame small (it
23004            // sits on the recursive nesting-budget cycle).
23005            Token::Ident(s) | Token::QuotedIdent(s)
23006                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
23007            {
23008                self.parse_array_subquery()
23009            }
23010            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
23011            // is not a reserved token; we match by case-insensitive
23012            // ident. The opening `[` must follow immediately. v7.39 (read01
23013            // round 105) — the body moved out-of-line so its `Vec`/loop locals
23014            // leave parse_atom's frame (which sits on the nesting-budget cycle).
23015            Token::Ident(s) | Token::QuotedIdent(s)
23016                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
23017            {
23018                self.parse_array_literal_body()
23019            }
23020            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
23021            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
23022            // We special-case before the generic ident dispatch so
23023            // the AGAINST clause never reaches the function-call
23024            // loop (which would mis-read `(cols) AGAINST` as a
23025            // call with no trailing modifier). The shape is
23026            // rewritten to a Boolean OR over per-column
23027            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23028            // term)` so the existing FTS evaluator handles
23029            // semantics — the fulltext-GIN built at CREATE TABLE
23030            // time is currently a "real index that survives dump
23031            // round-trip"; the planner hook that actually uses
23032            // it for posting-list intersection lands in a later
23033            // sub-phase (Phase 2.2b) without touching this surface.
23034            Token::Ident(s) | Token::QuotedIdent(s)
23035                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
23036            {
23037                self.parse_match_against_atom()
23038            }
23039            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
23040            // v7.37.43-T4 — PG-unreserved keywords are legal column /
23041            // alias names in expression context too. `release` appears
23042            // in sentori `0003_partition_events.sql` as both a column
23043            // reference (SELECT … release …) and an INSERT column list
23044            // entry. Mirrors `expect_ident_like`'s expansion of the
23045            // identifier set.
23046            other if unreserved_keyword_text(&other).is_some() => {
23047                let s = unreserved_keyword_text(&other).unwrap();
23048                self.finish_ident_atom(s)
23049            }
23050            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
23051            // only inside `SET` before, so `SELECT @@autocommit` — which
23052            // every MySQL connector asks at handshake — was a parse error.
23053            // MariaDB accepts the bare, `@@session.` and `@@global.`
23054            // spellings alike and answers from the session's own value.
23055            Token::SessionVar(v) => {
23056                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
23057                // has nothing to do with a `@@` engine setting: its own
23058                // per-session namespace, and an unset one reads NULL instead
23059                // of raising. Stripping every `@` (as this did) made `@x` and
23060                // `@@x` the same node, so `SELECT @x` answered "Unknown
23061                // system variable".
23062                Ok(variable_ref_atom(&v))
23063            }
23064            other => Err(ParseError {
23065                message: format!("unexpected token {other:?} in expression"),
23066                token_pos: tok_pos,
23067            }),
23068        }
23069        // After parsing the atom, fold any postfix `::vector` casts.
23070        .and_then(|atom| self.finish_postfix_casts(atom))
23071    }
23072
23073    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
23074    /// Both bind tighter than any binary op.
23075    /// Shared cast-target parser for postfix `::TYPE` and the
23076    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
23077    /// If the next tokens are `( N )`, consume them and return the canonical
23078    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
23079    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
23080    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
23081        if !matches!(self.peek(), Token::LParen) {
23082            return None;
23083        }
23084        self.advance(); // (
23085        let n = match self.advance() {
23086            Token::Integer(n) => n,
23087            _ => return Some(base.to_string()), // malformed → drop precision
23088        };
23089        if matches!(self.peek(), Token::RParen) {
23090            self.advance();
23091        }
23092        Some(alloc::format!("{base}({n})"))
23093    }
23094
23095    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
23096        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
23097        // schema-qualifies every cast target, and `pg_catalog.X` names
23098        // exactly the builtin type X. Consume the qualifier and let
23099        // the ordinary target parse decide.
23100        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
23101            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
23102        {
23103            self.advance();
23104            self.advance();
23105        }
23106        let target = match self.advance() {
23107            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
23108                "int" | "integer" | "int4" => {
23109                    if matches!(self.peek(), Token::LBracket)
23110                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23111                    {
23112                        self.advance();
23113                        self.advance();
23114                        CastTarget::IntArray
23115                    } else {
23116                        CastTarget::Int
23117                    }
23118                }
23119                "bigint" | "int8" => {
23120                    if matches!(self.peek(), Token::LBracket)
23121                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23122                    {
23123                        self.advance();
23124                        self.advance();
23125                        CastTarget::BigIntArray
23126                    } else {
23127                        CastTarget::BigInt
23128                    }
23129                }
23130                "float" | "double" => CastTarget::Float,
23131                "text" => {
23132                    // v7.10.11 — `::TEXT[]` widens to TextArray.
23133                    if matches!(self.peek(), Token::LBracket)
23134                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23135                    {
23136                        self.advance();
23137                        self.advance();
23138                        CastTarget::TextArray
23139                    } else {
23140                        CastTarget::Text
23141                    }
23142                }
23143                "bool" | "boolean" => CastTarget::Bool,
23144                "vector" => CastTarget::Vector,
23145                "date" => CastTarget::Date,
23146                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
23147                // seconds precision through the Named path (the engine rounds
23148                // the sub-second field); bare `::timestamp` keeps the fast arm.
23149                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
23150                    Some(named) => CastTarget::Named(named),
23151                    None => CastTarget::Timestamp,
23152                },
23153                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
23154                    Some(named) => CastTarget::Named(named),
23155                    None => CastTarget::Timestamptz,
23156                },
23157                "interval" => CastTarget::Interval,
23158                "json" => CastTarget::Json,
23159                "jsonb" => CastTarget::Jsonb,
23160                // v7.39 (round 694) — these have dedicated CastTarget
23161                // variants, so they never reached the postfix `[]` handling
23162                // further down and `::regtype[]` was a SYNTAX error at the
23163                // `]`. PG has an array type for every scalar; take the
23164                // suffix here and hand the canonical `<ty>_array` name to
23165                // the engine, the same shape every other array cast uses.
23166                "regtype" if self.peek_postfix_array_brackets() => {
23167                    self.advance();
23168                    self.advance();
23169                    CastTarget::Named(alloc::string::String::from("regtype_array"))
23170                }
23171                "regclass" if self.peek_postfix_array_brackets() => {
23172                    self.advance();
23173                    self.advance();
23174                    CastTarget::Named(alloc::string::String::from("regclass_array"))
23175                }
23176                "regtype" => CastTarget::RegType,
23177                "regclass" => CastTarget::RegClass,
23178                // v7.12.0 — `::tsvector` / `::tsquery`.
23179                // Engine decodes the LHS text via the PG
23180                // external form parser.
23181                // v7.39 (round 352, M8) — MySQL's own cast targets.
23182                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
23183                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
23184                // such type, so they are taken only in that dialect and
23185                // fall through to the "type does not exist" arm otherwise.
23186                "signed" | "unsigned" if self.mysql_dialect => {
23187                    if matches!(self.peek(), Token::Ident(k)
23188                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
23189                    {
23190                        self.advance();
23191                    }
23192                    CastTarget::Named(s.to_ascii_lowercase())
23193                }
23194                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
23195                // in MySQL: MariaDB answers '123' where the SQL-standard
23196                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
23197                // Truncating a number to its first digit is a wrong answer
23198                // with no error, so the MySQL session gets MySQL's reading.
23199                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
23200                    CastTarget::Text
23201                }
23202                "tsvector" => CastTarget::TsVector,
23203                "tsquery" => CastTarget::TsQuery,
23204                // v7.17.0 — `::uuid`. Engine decodes the LHS
23205                // text via `spg_storage::parse_uuid_str`.
23206                "uuid" => CastTarget::Uuid,
23207                // v7.18 — `::bytea`. Engine decodes the LHS
23208                // text via the PG hex form (`'\xdeadbeef'`)
23209                // or escape form (`'\\x05\\x00'`). Closes
23210                // mailrs D-pre #3 reverse-acceptance gap.
23211                "bytea" => CastTarget::Bytea,
23212                // v7.37.5 ship triage — generic typed-cast escape.
23213                // Anything the long-tail PG type ident table knows
23214                // about(network/bit/geometry/multirange/etc.)flows
23215                // through `CastTarget::Named(canonical)`; the engine
23216                // resolves via `column_type_to_data_type` and dispatches
23217                // through the typed `coerce_value` path. Truly
23218                // unrecognised idents still hit the error arm below
23219                // because the engine rejects them.
23220                other => {
23221                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
23222                    // `::varchar(255)`, etc. Capture into the canonical
23223                    // `name(p,s)` form so `type_name_to_data_type` can
23224                    // reconstruct the `DataType::Numeric { precision,
23225                    // scale }` (and similar param-carrying types).
23226                    let mut name = other.to_string();
23227                    // v7.39 (round 281) — `::bit varying(3)` is two
23228                    // words; fold the tail in so the typmod reaches the
23229                    // type resolver instead of tripping the parser.
23230                    if name.eq_ignore_ascii_case("bit")
23231                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23232                    {
23233                        self.advance();
23234                        name = alloc::string::String::from("varbit");
23235                    }
23236                    // v7.39 (round 613) — `::character varying` is the same
23237                    // two-word shape and had no fold, so the `varying` was
23238                    // left behind and the cast became a bare `character`,
23239                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
23240                    // `a` where PG answers `ab`. Silently, and for a spelling
23241                    // pg_dump writes.
23242                    if name.eq_ignore_ascii_case("character")
23243                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23244                    {
23245                        self.advance();
23246                        name = alloc::string::String::from("varchar");
23247                    }
23248                    if matches!(self.peek(), Token::LParen) {
23249                        let mut buf = alloc::string::String::from("(");
23250                        let mut depth = 0usize;
23251                        loop {
23252                            match self.advance() {
23253                                Token::LParen => {
23254                                    depth += 1;
23255                                    if depth > 1 {
23256                                        buf.push('(');
23257                                    }
23258                                }
23259                                Token::RParen => {
23260                                    depth -= 1;
23261                                    if depth == 0 {
23262                                        buf.push(')');
23263                                        break;
23264                                    }
23265                                    buf.push(')');
23266                                }
23267                                Token::Comma => buf.push(','),
23268                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
23269                                // v7.39 (round 273) — a minus used to fall
23270                                // into the catch-all below and vanish, so
23271                                // `::numeric(10,-2)` reached the engine as
23272                                // the text `numeric(10,2)` and silently
23273                                // rounded to two DECIMALS instead of to
23274                                // hundreds. A dropped token is not a
23275                                // no-op when it carries a sign.
23276                                Token::Minus => buf.push('-'),
23277                                Token::Eof => break,
23278                                _ => {}
23279                            }
23280                        }
23281                        name.push_str(&buf);
23282                    }
23283                    // Optional postfix `[]` widens to the array form —
23284                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
23285                    // The engine's `type_name_to_data_type` recognises
23286                    // the canonical `<ty>_array` form.
23287                    if matches!(self.peek(), Token::LBracket)
23288                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23289                    {
23290                        self.advance();
23291                        self.advance();
23292                        name.push_str("_array");
23293                    }
23294                    CastTarget::Named(name)
23295                }
23296            },
23297            Token::Interval => CastTarget::Interval,
23298            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
23299            // "char" (oid 18, SPG Char1 — distinct from bare `char`
23300            // = char(1)); other quoted names resolve like idents.
23301            Token::QuotedIdent(q) => {
23302                if q.eq_ignore_ascii_case("char") {
23303                    CastTarget::Named("char1".into())
23304                } else {
23305                    CastTarget::Named(q.to_ascii_lowercase())
23306                }
23307            }
23308            other => {
23309                return Err(ParseError {
23310                    message: format!("expected type ident after `::`, got {other:?}"),
23311                    token_pos: self.consumed_pos(),
23312                });
23313            }
23314        };
23315        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
23316        // target to its array sibling. Closed-enum arms (Bool /
23317        // SmallInt / Numeric / Float / Date / …) didn't carry the
23318        // explicit widening that Text / Int / BigInt did, so
23319        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
23320        // error. The widening here mirrors the per-arm Text /
23321        // Int / BigInt logic above + folds the new ζ-A first-class
23322        // types through `CastTarget::Named("<ty>_array")`.
23323        if matches!(self.peek(), Token::LBracket)
23324            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23325        {
23326            let widened = match &target {
23327                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
23328                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
23329                // v7.39 (round 326, V43) — the two temporal types stay
23330                // distinct. Both used to widen to `timestamptz_array`, so
23331                // `::timestamp[]` named the wrong target in its own error
23332                // message and lost the zone-less identity on the way.
23333                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
23334                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
23335                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
23336                CastTarget::Json | CastTarget::Jsonb => {
23337                    Some(CastTarget::Named("jsonb_array".to_string()))
23338                }
23339                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
23340                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
23341                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
23342                CastTarget::Named(name) => {
23343                    let mut a = name.clone();
23344                    a.push_str("_array");
23345                    Some(CastTarget::Named(a))
23346                }
23347                // Int / BigInt / Text / Vector / TsVector / TsQuery /
23348                // RegType / RegClass / TextArray / IntArray /
23349                // BigIntArray already finalised — leave as is.
23350                _ => None,
23351            };
23352            if let Some(w) = widened {
23353                self.advance();
23354                self.advance();
23355                return Ok(w);
23356            }
23357        }
23358        Ok(target)
23359    }
23360
23361    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
23362        loop {
23363            // v7.38 (read01, T9) — composite field access `(expr).field`.
23364            // A bare `a.b` is consumed as a qualified column inside the ident
23365            // atom, so a Dot only survives to this postfix position when the
23366            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
23367            // `.*` whole-row expansion is not handled here (projection-level).
23368            if matches!(self.peek(), Token::Dot)
23369                && matches!(
23370                    self.tokens.get(self.pos + 1),
23371                    Some(Token::Ident(_) | Token::QuotedIdent(_))
23372                )
23373            {
23374                self.advance(); // .
23375                let field = match self.advance() {
23376                    Token::Ident(s) | Token::QuotedIdent(s) => s,
23377                    other => {
23378                        return Err(
23379                            self.err(format!("expected a field name after '.', got {other:?}"))
23380                        );
23381                    }
23382                };
23383                expr = Expr::FieldAccess {
23384                    base: Box::new(expr),
23385                    field,
23386                };
23387                continue;
23388            }
23389            if matches!(self.peek(), Token::DoubleColon) {
23390                self.advance();
23391                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
23392                // target set to include INTERVAL (reserved Token),
23393                // TIMESTAMPTZ, and PG catalog regtype / regclass.
23394                // mailrs follow-up H3a + H3b.
23395                let target = self.parse_cast_target()?;
23396                expr = Expr::Cast {
23397                    expr: Box::new(expr),
23398                    target,
23399                };
23400                continue;
23401            }
23402            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23403            // returns NULL for out-of-range. Multiple subscripts
23404            // chain: `a[i][j]` parses left-to-right.
23405            if matches!(self.peek(), Token::LBracket) {
23406                self.advance();
23407                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23408                // bare index stays a subscript.
23409                let lo = if matches!(self.peek(), Token::Colon) {
23410                    None
23411                } else {
23412                    Some(self.parse_expr(0)?)
23413                };
23414                if matches!(self.peek(), Token::Colon) {
23415                    self.advance();
23416                    let hi = if matches!(self.peek(), Token::RBracket) {
23417                        None
23418                    } else {
23419                        Some(Box::new(self.parse_expr(0)?))
23420                    };
23421                    if !matches!(self.peek(), Token::RBracket) {
23422                        return Err(self.err(alloc::format!(
23423                            "expected ']' after array slice, got {:?}",
23424                            self.peek()
23425                        )));
23426                    }
23427                    self.advance();
23428                    expr = Expr::ArraySlice {
23429                        target: Box::new(expr),
23430                        lo: lo.map(Box::new),
23431                        hi,
23432                    };
23433                    continue;
23434                }
23435                let index = lo.expect("non-colon branch parsed an index");
23436                if !matches!(self.peek(), Token::RBracket) {
23437                    return Err(self.err(alloc::format!(
23438                        "expected ']' after array index, got {:?}",
23439                        self.peek()
23440                    )));
23441                }
23442                self.advance();
23443                expr = Expr::ArraySubscript {
23444                    target: Box::new(expr),
23445                    index: Box::new(index),
23446                };
23447                continue;
23448            }
23449            // `expr AT TIME ZONE zone` — lowers to PG's own function
23450            // form timezone(zone, expr); the scalar implements the
23451            // offset shift (named zones error there — no tzdata).
23452            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23453                && matches!(self.tokens.get(self.pos + 1),
23454                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23455                && matches!(self.tokens.get(self.pos + 2),
23456                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23457            {
23458                self.advance(); // AT
23459                self.advance(); // TIME
23460                self.advance(); // ZONE
23461                // Zone at comparison precedence so AND/OR stay out.
23462                let zone = self.parse_expr(6)?;
23463                expr = Expr::FunctionCall {
23464                    name: "timezone".to_string(),
23465                    args: alloc::vec![zone, expr],
23466                };
23467                continue;
23468            }
23469            // `expr COLLATE "name"` — SPG's single text ordering IS
23470            // byte order, i.e. the C collation. The byte-order
23471            // spellings absorb as no-ops; a locale collation would
23472            // silently sort differently from PG, so it errors
23473            // honestly instead.
23474            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23475                self.advance();
23476                let mut cname = match self.advance() {
23477                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23478                    other => {
23479                        return Err(self.err(alloc::format!(
23480                            "expected collation name after COLLATE, got {other:?}"
23481                        )));
23482                    }
23483                };
23484                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23485                // is how `pg_dump` writes the default one:
23486                // `… COLLATE pg_catalog.default`. Reading a single token
23487                // left the SCHEMA as the name, so the clause was refused
23488                // as an unsupported locale collation and no dump ran.
23489                if matches!(self.peek(), Token::Dot) {
23490                    // v7.39.2 — the qualifier is DROPPED (SPG is single
23491                    // schema) but it is checked first. PostgreSQL 18.6
23492                    // answers `schema "nosuch_schema" does not exist` for
23493                    // one it has never heard of, and dropping it unread
23494                    // meant `COLLATE nosuch_schema."C"` succeeded here —
23495                    // a name that names nothing, accepted.
23496                    let schema = cname.to_ascii_lowercase();
23497                    if !matches!(
23498                        schema.as_str(),
23499                        "pg_catalog" | "public" | "information_schema"
23500                    ) {
23501                        return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23502                    }
23503                    self.advance();
23504                    cname = match self.advance() {
23505                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23506                        // `default` lexes as a KEYWORD, and it is the name
23507                        // pg_dump writes — the same trap round 535 hit with
23508                        // TABLE / INDEX / FULL.
23509                        Token::Default => alloc::string::String::from("default"),
23510                        other => {
23511                            return Err(self.err(alloc::format!(
23512                                "expected collation name after COLLATE, got {other:?}"
23513                            )));
23514                        }
23515                    };
23516                }
23517                let lc = cname.to_ascii_lowercase();
23518                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23519                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23520                // family / `binary`) forces byte-wise, which is exactly
23521                // what `BINARY expr` does — lower onto that so every fold
23522                // site (comparison, LIKE, ORDER BY) suppresses via
23523                // `is_binary_coerced`. A `_ci` family override folds, and
23524                // under the MySQL dialect the default already folds, so it
23525                // absorbs as a no-op; likewise the C / byte-order spellings.
23526                // v7.39.2 — against MySQL's own list, not against the
23527                // shape of the name. `nosuch_bin` took this shortcut and
23528                // became a BINARY cast; `nosuch_ci` took the one below
23529                // and was absorbed as a no-op. Either way the client
23530                // named a collation that does not exist and was told
23531                // nothing. An unknown name now falls through to the
23532                // node, and the engine refuses it.
23533                let real = crate::charset::is_mysql_collation(&lc);
23534                // v7.40.0 — `binary` lowers; a `_bin` COLLATION does not.
23535                //
23536                // They are not the same thing, and folding them together
23537                // lost a bit. Measured on MySQL 9.7.2 with the connection
23538                // on utf8mb4:
23539                //
23540                // ```text
23541                //   'a ' = 'a' COLLATE utf8mb4_bin        1   PAD SPACE
23542                //   'a ' = 'a' COLLATE utf8mb4_0900_bin   0   NO PAD
23543                //   'AB' = 'ab' COLLATE utf8mb4_bin       0   byte-wise
23544                // ```
23545                //
23546                // The BINARY cast carries "byte-wise" and, with it,
23547                // "no pad" — so `utf8mb4_bin`, which pads, answered 0 to
23548                // the first line. Keeping the node lets `text_compare_of`
23549                // read the NAME and settle the two bits separately: it
23550                // does not fold (`folds_case` says so) and it does pad
23551                // (`pads_space` says so), while `is_byte_wise` still
23552                // keeps the ORDERING off the locale.
23553                if self.mysql_dialect && real && lc == "binary" {
23554                    expr = Expr::Cast {
23555                        expr: alloc::boxed::Box::new(expr),
23556                        target: CastTarget::Named("binary".to_string()),
23557                    };
23558                    continue;
23559                }
23560                let mysql_ci = self.mysql_dialect
23561                    && ((real && lc.ends_with("_ci"))
23562                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23563                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23564                // goes to the lowering channel, the byte-order spellings
23565                // included. Round 691 recorded only the names the old
23566                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23567                // absorbed as a no-op — and once a column could declare a
23568                // collation, absorbing the clause meant the COLUMN's
23569                // collation won where the query had asked for bytes.
23570                if self.in_order_by_key && !mysql_ci {
23571                    self.order_key_collation = Some(cname);
23572                    continue;
23573                }
23574                // v7.39.2 — the clause becomes a NODE rather than being
23575                // refused or absorbed.
23576                //
23577                // What stood here refused the locale names and SILENTLY
23578                // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23579                // answered `t` where PostgreSQL 18.6 answers `f`: the one
23580                // family it let through is the one where dropping it
23581                // changes the answer. Absorbing is only correct when the
23582                // collation asked for is the one the comparison would use
23583                // anyway, and that depends on the DATABASE — which the
23584                // parser cannot see. So it rides along and the engine,
23585                // which can, decides.
23586                //
23587                // `collate_derive` already modelled `Explicit(name)` and
23588                // had no way to be handed one.
23589                // v7.39.2 — a MySQL spelling does not exist on the
23590                // PostgreSQL wire, and THIS is where the wire is known.
23591                //
23592                // The check lived in the evaluator first and asked
23593                // `ctx.mysql_dialect`, which the INSERT path builds as a
23594                // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23595                // in a MySQL session was refused for a collation that
23596                // does not exist on a wire it was not on. Making that
23597                // context truthful would change INSERT-time evaluation
23598                // in other ways as a side effect; the parser already
23599                // gates the introducer on the same flag and is the
23600                // honest place to ask.
23601                if !self.mysql_dialect
23602                    && (lc.ends_with("_ci")
23603                        || lc.ends_with("_cs")
23604                        || lc.ends_with("_bin")
23605                        || lc == "binary"
23606                        || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23607                {
23608                    return Err(self.err(alloc::format!(
23609                        "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23610                    )));
23611                }
23612                // v7.39.3 — the node is built for EVERY name, `_ci`
23613                // included.
23614                //
23615                // A MySQL `_ci` spelling used to be absorbed here on the
23616                // reasoning that a MySQL session folds anyway, so the
23617                // clause asked for what it would have got. That stopped
23618                // being true when the fold learned to read the session's
23619                // collation NAME: under `SET NAMES utf8mb4 COLLATE
23620                // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23621                // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23622                // that would have made it 1 had been dropped in the
23623                // parser. Absorbing is only ever correct when the
23624                // collation asked for is the one the comparison would use
23625                // anyway, and the parser cannot know that — the same
23626                // reasoning already written above for the byte-order
23627                // spellings, applied to the family it had exempted.
23628                expr = Expr::Collate {
23629                    expr: alloc::boxed::Box::new(expr),
23630                    collation: cname,
23631                };
23632                continue;
23633            }
23634            return Ok(expr);
23635        }
23636    }
23637
23638    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23639    /// the first token that is not one. Schema qualifiers collapse to the
23640    /// last part, which is what every other name path here does (SPG is
23641    /// single-schema).
23642    fn take_comma_separated_names(&mut self) -> Vec<String> {
23643        let mut out = Vec::new();
23644        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23645            self.advance();
23646            let mut last = n;
23647            while matches!(self.peek(), Token::Dot) {
23648                self.advance();
23649                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23650                    last = t;
23651                }
23652            }
23653            out.push(last);
23654            if matches!(self.peek(), Token::Comma) {
23655                self.advance();
23656            } else {
23657                break;
23658            }
23659        }
23660        out
23661    }
23662
23663    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23664    ///
23665    /// The general cast-target path tests this inline; the types with their
23666    /// own `CastTarget` variant need it as a guard on their match arm,
23667    /// which is what this exists for.
23668    fn peek_postfix_array_brackets(&self) -> bool {
23669        matches!(self.peek(), Token::LBracket)
23670            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23671    }
23672
23673    /// Parse the operator tail after a `(a, b, …)` row constructor
23674    /// and expand at parse time. `=` is the conjunction of element
23675    /// equalities; `<>` its negation; the order operators expand
23676    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23677    /// equalities. Anything else (a bare row value, a subquery
23678    /// RHS) errors honestly — SPG has no composite runtime value.
23679    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23680        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23681            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23682                lhs: Box::new(l.clone()),
23683                op: BinOp::Eq,
23684                rhs: Box::new(r.clone()),
23685            });
23686            let first = it.next().expect("row has at least two elements");
23687            it.fold(first, |acc, e| Expr::Binary {
23688                lhs: Box::new(acc),
23689                op: BinOp::And,
23690                rhs: Box::new(e),
23691            })
23692        }
23693        // Lexicographic (a,b) OP (c,d):
23694        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
23695        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23696            if lhs.len() == 1 {
23697                return Expr::Binary {
23698                    lhs: Box::new(lhs[0].clone()),
23699                    op: last,
23700                    rhs: Box::new(rhs[0].clone()),
23701                };
23702            }
23703            let head_strict = Expr::Binary {
23704                lhs: Box::new(lhs[0].clone()),
23705                op: strict,
23706                rhs: Box::new(rhs[0].clone()),
23707            };
23708            let head_eq = Expr::Binary {
23709                lhs: Box::new(lhs[0].clone()),
23710                op: BinOp::Eq,
23711                rhs: Box::new(rhs[0].clone()),
23712            };
23713            Expr::Binary {
23714                lhs: Box::new(head_strict),
23715                op: BinOp::Or,
23716                rhs: Box::new(Expr::Binary {
23717                    lhs: Box::new(head_eq),
23718                    op: BinOp::And,
23719                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23720                }),
23721            }
23722        }
23723        let negated_in = if matches!(self.peek(), Token::Not)
23724            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23725        {
23726            self.advance();
23727            true
23728        } else {
23729            false
23730        };
23731        if matches!(self.peek(), Token::In) {
23732            self.advance();
23733            if !matches!(self.peek(), Token::LParen) {
23734                return Err(self.err(alloc::format!(
23735                    "expected '(' after row IN, got {:?}",
23736                    self.peek()
23737                )));
23738            }
23739            self.advance();
23740            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23741            // not a list of literal rows. Row-vs-list decomposes to
23742            // OR-of-AND above, but the subquery's rows are only known at
23743            // runtime, so keep it as a RowInSubquery node.
23744            if matches!(self.peek(), Token::Select) {
23745                let inner = self.parse_select_stmt()?;
23746                if !matches!(self.peek(), Token::RParen) {
23747                    return Err(self.err(alloc::format!(
23748                        "expected ')' after row IN-subquery, got {:?}",
23749                        self.peek()
23750                    )));
23751                }
23752                self.advance();
23753                let Statement::Select(s) = inner else {
23754                    unreachable!("parse_select_stmt always returns Statement::Select")
23755                };
23756                return Ok(Expr::RowInSubquery {
23757                    row,
23758                    subquery: Box::new(s),
23759                    negated: negated_in,
23760                });
23761            }
23762            let mut alternatives: Vec<Expr> = Vec::new();
23763            loop {
23764                // Optional ROW keyword before the paren row.
23765                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23766                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23767                {
23768                    self.advance();
23769                }
23770                if !matches!(self.peek(), Token::LParen) {
23771                    return Err(self.err(alloc::format!(
23772                        "expected '(' to open a row inside IN, got {:?}",
23773                        self.peek()
23774                    )));
23775                }
23776                self.advance();
23777                let mut rhs = alloc::vec![self.parse_expr(0)?];
23778                while matches!(self.peek(), Token::Comma) {
23779                    self.advance();
23780                    rhs.push(self.parse_expr(0)?);
23781                }
23782                if !matches!(self.peek(), Token::RParen) {
23783                    return Err(self.err(alloc::format!(
23784                        "expected ')' after row inside IN, got {:?}",
23785                        self.peek()
23786                    )));
23787                }
23788                self.advance();
23789                if rhs.len() != row.len() {
23790                    return Err(self.err(alloc::format!(
23791                        "row IN arity mismatch: left has {}, right has {}",
23792                        row.len(),
23793                        rhs.len()
23794                    )));
23795                }
23796                alternatives.push(row_eq(&row, &rhs));
23797                if matches!(self.peek(), Token::Comma) {
23798                    self.advance();
23799                    continue;
23800                }
23801                break;
23802            }
23803            if !matches!(self.peek(), Token::RParen) {
23804                return Err(self.err(alloc::format!(
23805                    "expected ')' to close row IN list, got {:?}",
23806                    self.peek()
23807                )));
23808            }
23809            self.advance();
23810            let mut it = alternatives.into_iter();
23811            let first = it.next().expect("IN list has at least one row");
23812            let combined = it.fold(first, |acc, e| Expr::Binary {
23813                lhs: Box::new(acc),
23814                op: BinOp::Or,
23815                rhs: Box::new(e),
23816            });
23817            return Ok(maybe_not(combined, negated_in));
23818        }
23819        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23820        // two periods share at least one time point. Each pair is
23821        // normalised with least/greatest (PG accepts the endpoints
23822        // in either order), then lowered to the standard
23823        // `start1 < end2 AND start2 < end1` form.
23824        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23825            if row.len() != 2 {
23826                return Err(self.err(alloc::format!(
23827                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
23828                    row.len()
23829                )));
23830            }
23831            self.advance();
23832            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23833                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23834            {
23835                self.advance();
23836            }
23837            if !matches!(self.peek(), Token::LParen) {
23838                return Err(self.err(alloc::format!(
23839                    "expected '(' after OVERLAPS, got {:?}",
23840                    self.peek()
23841                )));
23842            }
23843            self.advance();
23844            let r0 = self.parse_expr(0)?;
23845            if !matches!(self.peek(), Token::Comma) {
23846                return Err(self.err(alloc::format!(
23847                    "OVERLAPS needs (start, end) on the right, got {:?}",
23848                    self.peek()
23849                )));
23850            }
23851            self.advance();
23852            let r1 = self.parse_expr(0)?;
23853            if !matches!(self.peek(), Token::RParen) {
23854                return Err(self.err(alloc::format!(
23855                    "expected ')' after OVERLAPS pair, got {:?}",
23856                    self.peek()
23857                )));
23858            }
23859            self.advance();
23860            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23861                name: String::from(name),
23862                args: alloc::vec![a.clone(), b.clone()],
23863            };
23864            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23865                lhs: Box::new(lhs),
23866                op: BinOp::Lt,
23867                rhs: Box::new(rhs),
23868            };
23869            return Ok(Expr::Binary {
23870                lhs: Box::new(lt(
23871                    pair_fn("least", &row[0], &row[1]),
23872                    pair_fn("greatest", &r0, &r1),
23873                )),
23874                op: BinOp::And,
23875                rhs: Box::new(lt(
23876                    pair_fn("least", &r0, &r1),
23877                    pair_fn("greatest", &row[0], &row[1]),
23878                )),
23879            });
23880        }
23881        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23882        // PG, `IS NULL` is true only when EVERY field is NULL, and
23883        // `IS NOT NULL` is true only when every field is non-NULL — the
23884        // latter is NOT the negation of the former (a mixed row is
23885        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23886        // which reproduces exactly that all-fields semantics.
23887        if matches!(self.peek(), Token::Is) {
23888            self.advance();
23889            let negated = if matches!(self.peek(), Token::Not) {
23890                self.advance();
23891                true
23892            } else {
23893                false
23894            };
23895            if !matches!(self.peek(), Token::Null) {
23896                return Err(self.err(alloc::format!(
23897                    "expected NULL after row IS [NOT], got {:?}",
23898                    self.peek()
23899                )));
23900            }
23901            self.advance();
23902            let mut it = row.iter().map(|e| Expr::IsNull {
23903                expr: Box::new(e.clone()),
23904                negated,
23905            });
23906            let first = it.next().expect("row has at least two elements");
23907            return Ok(it.fold(first, |acc, e| Expr::Binary {
23908                lhs: Box::new(acc),
23909                op: BinOp::And,
23910                rhs: Box::new(e),
23911            }));
23912        }
23913        let op = match self.peek() {
23914            Token::Eq => BinOp::Eq,
23915            Token::NotEq => BinOp::NotEq,
23916            Token::Lt => BinOp::Lt,
23917            Token::LtEq => BinOp::LtEq,
23918            Token::Gt => BinOp::Gt,
23919            Token::GtEq => BinOp::GtEq,
23920            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
23921            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
23922            // constructor value, identical to the `ROW(a, b, …)` keyword form:
23923            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
23924            // (`::text`, `.field`) applies at the caller just as it does for the
23925            // ROW(...) node. All the comparison / predicate forms returned above.
23926            _ => {
23927                return Ok(Expr::FunctionCall {
23928                    name: String::from("row"),
23929                    args: row,
23930                });
23931            }
23932        };
23933        self.advance();
23934        // Optional ROW keyword before the paren row.
23935        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23936            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23937        {
23938            self.advance();
23939        }
23940        if !matches!(self.peek(), Token::LParen) {
23941            return Err(self.err(alloc::format!(
23942                "expected '(' to open the right-hand row, got {:?}",
23943                self.peek()
23944            )));
23945        }
23946        self.advance();
23947        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
23948        // subquery. Kept as a node (the subquery's row is a runtime value);
23949        // the literal-RHS form below still decomposes at parse time.
23950        if matches!(self.peek(), Token::Select) {
23951            let inner = self.parse_select_stmt()?;
23952            if !matches!(self.peek(), Token::RParen) {
23953                return Err(self.err(alloc::format!(
23954                    "expected ')' after row comparison subquery, got {:?}",
23955                    self.peek()
23956                )));
23957            }
23958            self.advance();
23959            let Statement::Select(s) = inner else {
23960                unreachable!("parse_select_stmt always returns Statement::Select")
23961            };
23962            return Ok(Expr::RowCmpSubquery {
23963                row,
23964                op,
23965                subquery: Box::new(s),
23966            });
23967        }
23968        let mut rhs = alloc::vec![self.parse_expr(0)?];
23969        while matches!(self.peek(), Token::Comma) {
23970            self.advance();
23971            rhs.push(self.parse_expr(0)?);
23972        }
23973        if !matches!(self.peek(), Token::RParen) {
23974            return Err(self.err(alloc::format!(
23975                "expected ')' after right-hand row, got {:?}",
23976                self.peek()
23977            )));
23978        }
23979        self.advance();
23980        if rhs.len() != row.len() {
23981            // v7.39 (round 239) — PG's wording (42601).
23982            return Err(self.err("unequal number of entries in row expressions".to_string()));
23983        }
23984        Ok(match op {
23985            BinOp::Eq => row_eq(&row, &rhs),
23986            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
23987            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
23988            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
23989            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
23990            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
23991            _ => unreachable!("op restricted above"),
23992        })
23993    }
23994
23995    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
23996    /// escape character becomes the matcher's default backslash:
23997    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
23998    /// → the char itself, and any pre-existing backslash escapes
23999    /// itself so it stays literal. Both operands must be string
24000    /// literals — a runtime pattern would need matcher support.
24001    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
24002        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
24003            (&pattern, &esc)
24004        else {
24005            return Err(
24006                "LIKE ... ESCAPE requires string-literal pattern and escape \
24007                 (runtime escape characters are not supported yet)"
24008                    .into(),
24009            );
24010        };
24011        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
24012        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
24013        // multi-character escape is an error.
24014        let esc_ch: Option<char> = {
24015            let mut ch_iter = e.chars();
24016            match (ch_iter.next(), ch_iter.next()) {
24017                (Some(c), None) => Some(c),
24018                (None, _) => None,
24019                (Some(_), Some(_)) => {
24020                    return Err(alloc::format!(
24021                        "ESCAPE must be a single character, got {e:?}"
24022                    ));
24023                }
24024            }
24025        };
24026        let mut out = String::with_capacity(p.len() + 4);
24027        let mut chars = p.chars();
24028        while let Some(c) = chars.next() {
24029            if Some(c) == esc_ch {
24030                match chars.next() {
24031                    // Escaped wildcard / escaped escape → keep the
24032                    // next char literal via backslash.
24033                    Some(next) => {
24034                        out.push('\\');
24035                        out.push(next);
24036                    }
24037                    None => {
24038                        return Err("LIKE pattern ends with the escape character".into());
24039                    }
24040                }
24041            } else if c == '\\' && esc_ch != Some('\\') {
24042                // A raw backslash is literal under a custom (or absent) escape
24043                // — escape it for the backslash-based matcher.
24044                out.push('\\');
24045                out.push('\\');
24046            } else {
24047                out.push(c);
24048            }
24049        }
24050        Ok(Expr::Literal(Literal::String(out)))
24051    }
24052
24053    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
24054    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
24055    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
24056    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
24057    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
24058    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
24059    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
24060    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
24061    /// array expression errors honestly rather than silently mismatching.
24062    fn try_like_any_all(
24063        &mut self,
24064        base: &Expr,
24065        negated: bool,
24066        case_insensitive: bool,
24067    ) -> Result<Option<Expr>, ParseError> {
24068        let is_any = match self.peek() {
24069            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
24070            Token::Ident(s)
24071                if s.eq_ignore_ascii_case("any")
24072                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
24073            {
24074                true
24075            }
24076            _ => return Ok(None),
24077        };
24078        self.advance(); // ANY / ALL
24079        self.advance(); // '('
24080        let arr = self.parse_expr(0)?;
24081        if !matches!(self.peek(), Token::RParen) {
24082            return Err(self.err(format!(
24083                "expected ')' after LIKE {} argument, got {:?}",
24084                if is_any { "ANY" } else { "ALL" },
24085                self.peek()
24086            )));
24087        }
24088        self.advance(); // ')'
24089        let Expr::Array(items) = arr else {
24090            return Err(self.err(
24091                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
24092            ));
24093        };
24094        let mut clauses = items.into_iter().map(|p| Expr::Like {
24095            expr: Box::new(base.clone()),
24096            pattern: Box::new(p),
24097            negated,
24098            case_insensitive,
24099        });
24100        let Some(first) = clauses.next() else {
24101            // ANY(empty) = FALSE, ALL(empty) = TRUE.
24102            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
24103        };
24104        let op = if is_any { BinOp::Or } else { BinOp::And };
24105        let combined = clauses.fold(first, |acc, c| Expr::Binary {
24106            lhs: Box::new(acc),
24107            op,
24108            rhs: Box::new(c),
24109        });
24110        Ok(Some(combined))
24111    }
24112
24113    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
24114    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
24115    /// `AND` is not swallowed.
24116    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24117        self.advance(); // BETWEEN
24118        // SYMMETRIC — the bounds may arrive in either order; both
24119        // orientations OR together. ASYMMETRIC is the default and
24120        // absorbs as noise.
24121        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
24122        {
24123            self.advance();
24124            true
24125        } else {
24126            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
24127                self.advance();
24128            }
24129            false
24130        };
24131        let low = self.parse_expr(6)?;
24132        if !matches!(self.peek(), Token::And) {
24133            return Err(self.err(format!(
24134                "expected AND after BETWEEN low bound, got {:?}",
24135                self.peek()
24136            )));
24137        }
24138        self.advance();
24139        let high = self.parse_expr(6)?;
24140        let target = Box::new(expr);
24141        let range = |lo: Expr, hi: Expr| Expr::Binary {
24142            lhs: Box::new(Expr::Binary {
24143                lhs: target.clone(),
24144                op: BinOp::GtEq,
24145                rhs: Box::new(lo),
24146            }),
24147            op: BinOp::And,
24148            rhs: Box::new(Expr::Binary {
24149                lhs: target.clone(),
24150                op: BinOp::LtEq,
24151                rhs: Box::new(hi),
24152            }),
24153        };
24154        let combined = if symmetric {
24155            Expr::Binary {
24156                lhs: Box::new(range(low.clone(), high.clone())),
24157                op: BinOp::Or,
24158                rhs: Box::new(range(high, low)),
24159            }
24160        } else {
24161            range(low, high)
24162        };
24163        Ok(maybe_not(combined, negated))
24164    }
24165
24166    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
24167    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
24168    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
24169    /// Caller already consumed the leading `WITH` ident.
24170    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
24171    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
24172    /// self-reference that appears more than once in a single term.
24173    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
24174        use crate::ast::{CteBody, SelectStatement};
24175        if !cte.recursive {
24176            return Ok(());
24177        }
24178        let CteBody::Select(body) = &cte.body else {
24179            return Ok(());
24180        };
24181        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
24182        // check the anchor and every peer term.
24183        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
24184        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
24185        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
24186            return Err(self.err(String::from(
24187                "ORDER BY in a recursive query is not implemented",
24188            )));
24189        }
24190        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
24191            return Err(self.err(String::from(
24192                "LIMIT in a recursive query is not implemented",
24193            )));
24194        }
24195        let self_refs = |s: &SelectStatement| -> usize {
24196            let Some(from) = &s.from else {
24197                return 0;
24198            };
24199            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
24200            for j in &from.joins {
24201                if j.table.name.eq_ignore_ascii_case(&cte.name) {
24202                    n += 1;
24203                }
24204            }
24205            n
24206        };
24207        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
24208            return Err(self.err(alloc::format!(
24209                "recursive reference to query \"{}\" must not appear more than once",
24210                cte.name
24211            )));
24212        }
24213        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
24214        // apply only when the body actually references itself (a non-self-
24215        // referencing CTE under WITH RECURSIVE may use any set-op shape).
24216        let anchor_refs = self_refs(body);
24217        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
24218        if anchor_refs > 0 || union_refs {
24219            // Shape: the top level must be UNION [ALL] arms only. A self-ref
24220            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
24221            // "does not have the form" error — SPG used to compute a value.
24222            if body.unions.is_empty()
24223                || body.unions.iter().any(|(k, _)| {
24224                    !matches!(
24225                        k,
24226                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
24227                    )
24228                })
24229            {
24230                return Err(self.err(alloc::format!(
24231                    "recursive query \"{}\" does not have the form non-recursive-term \
24232                     UNION [ALL] recursive-term",
24233                    cte.name
24234                )));
24235            }
24236            if anchor_refs > 0 {
24237                return Err(self.err(alloc::format!(
24238                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
24239                    cte.name
24240                )));
24241            }
24242        }
24243        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
24244        for (_, u) in &body.unions {
24245            if self_refs(u) == 0 {
24246                continue;
24247            }
24248            // The self-reference must not sit on the nullable side of an outer
24249            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
24250            if let Some(from) = &u.from {
24251                for (i, j) in from.joins.iter().enumerate() {
24252                    let left_has_self = is_self(&from.primary)
24253                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
24254                    let violated = match j.kind {
24255                        crate::ast::JoinKind::Left => is_self(&j.table),
24256                        crate::ast::JoinKind::Right => left_has_self,
24257                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
24258                        _ => false,
24259                    };
24260                    if violated {
24261                        return Err(self.err(alloc::format!(
24262                            "recursive reference to query \"{}\" must not appear within an outer join",
24263                            cte.name
24264                        )));
24265                    }
24266                }
24267            }
24268            // No aggregates at the top level of the recursive term (SPG used
24269            // to run them and surface a misleading downstream error).
24270            let mut items_and_having: Vec<&Expr> = Vec::new();
24271            for it in &u.items {
24272                if let crate::ast::SelectItem::Expr { expr, .. } = it {
24273                    items_and_having.push(expr);
24274                }
24275            }
24276            if let Some(h) = &u.having {
24277                items_and_having.push(h);
24278            }
24279            for e in items_and_having {
24280                if expr_has_toplevel_aggregate(e) {
24281                    return Err(self.err(String::from(
24282                        "aggregate functions are not allowed in a recursive query's recursive term",
24283                    )));
24284                }
24285            }
24286        }
24287        // A self-reference inside a sublink expression (EXISTS / IN / scalar
24288        // subquery) anywhere in the body is rejected; a plain FROM derived
24289        // table is legal in PG and untouched here.
24290        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
24291        all_terms.extend(body.unions.iter().map(|(_, u)| u));
24292        for term in all_terms {
24293            if select_has_self_ref_in_sublink(term, &cte.name) {
24294                return Err(self.err(alloc::format!(
24295                    "recursive reference to query \"{}\" must not appear within a subquery",
24296                    cte.name
24297                )));
24298            }
24299        }
24300        Ok(())
24301    }
24302
24303    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
24304    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
24305    /// right after parse so the engine sees a plain recursive CTE with the
24306    /// tracking columns already projected. DEPTH FIRST and CYCLE are
24307    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
24308    /// text-rendered rows can't provide, and errors honestly.
24309    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
24310        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
24311        if cte.search.is_none() && cte.cycle.is_none() {
24312            return Ok(());
24313        }
24314        let cte_name = cte.name.clone();
24315        let col_names = cte.column_overrides.clone();
24316        if col_names.is_empty() {
24317            return Err(
24318                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
24319            );
24320        }
24321        let search = cte.search.take();
24322        let cycle = cte.cycle.take();
24323        let mut extra_cols: Vec<String> = Vec::new();
24324        let col_ref = |name: &str| {
24325            Expr::Column(ColumnName {
24326                qualifier: Some(cte_name.clone()),
24327                name: name.to_string(),
24328            })
24329        };
24330        // Position of a SEARCH/CYCLE column within the CTE's column list.
24331        let pos_of = |name: &str| -> Result<usize, ParseError> {
24332            col_names
24333                .iter()
24334                .position(|c| c.eq_ignore_ascii_case(name))
24335                .ok_or_else(|| {
24336                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
24337                })
24338        };
24339        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
24340            let mut args = Vec::with_capacity(positions.len());
24341            for &p in positions {
24342                match items.get(p) {
24343                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
24344                    _ => {
24345                        return Err(self.err(
24346                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
24347                        ));
24348                    }
24349                }
24350            }
24351            Ok(Expr::FunctionCall {
24352                name: "row".into(),
24353                args,
24354            })
24355        };
24356        let CteBody::Select(body) = &mut cte.body else {
24357            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
24358        };
24359        if body.unions.is_empty() {
24360            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
24361        }
24362        let rec = body.unions.len() - 1; // recursive term = last UNION peer
24363
24364        if let Some(srch) = search {
24365            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
24366            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
24367            // no typed `record[]`, but element-wise array ORDER BY is correct
24368            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
24369            // exactly onto a typed array: DEPTH is the root→node path
24370            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
24371            // orders numerically (multi-digit keys included), matching PG.
24372            //
24373            // A multi-column BY would need a record[] to keep the per-node key
24374            // tuple orderable, which SPG can't express — error honestly there
24375            // rather than mis-order.
24376            if srch.by_columns.len() != 1 {
24377                return Err(self.err(
24378                    "SEARCH … BY with multiple columns needs typed record[] ordering \
24379                     SPG doesn't have yet; a single BY column is supported"
24380                        .into(),
24381                ));
24382            }
24383            let key_pos = pos_of(&srch.by_columns[0])?;
24384            let base_key = match body.items.get(key_pos) {
24385                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24386                _ => {
24387                    return Err(
24388                        self.err("SEARCH BY column maps to a non-expression select item".into())
24389                    );
24390                }
24391            };
24392            let rec_key = match body.unions[rec].1.items.get(key_pos) {
24393                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24394                _ => {
24395                    return Err(
24396                        self.err("SEARCH BY column maps to a non-expression select item".into())
24397                    );
24398                }
24399            };
24400            if srch.depth_first {
24401                // base: ARRAY[key]; rec: array_append(cte.set, key).
24402                body.items.push(SelectItem::Expr {
24403                    expr: Expr::Array(alloc::vec![base_key]),
24404                    alias: Some(srch.set_column.clone()),
24405                });
24406                body.unions[rec].1.items.push(SelectItem::Expr {
24407                    expr: Expr::FunctionCall {
24408                        name: "array_append".into(),
24409                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
24410                    },
24411                    alias: Some(srch.set_column.clone()),
24412                });
24413            } else {
24414                // BREADTH: [depth, key]; depth starts at 0 and increments. The
24415                // leading depth element dominates the element-wise comparison,
24416                // so shallower rows sort first, then by key — PG's (depth, key).
24417                body.items.push(SelectItem::Expr {
24418                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24419                    alias: Some(srch.set_column.clone()),
24420                });
24421                // rec depth = cte.set[1] + 1.
24422                let parent_depth = Expr::ArraySubscript {
24423                    target: Box::new(col_ref(&srch.set_column)),
24424                    index: Box::new(Expr::Literal(Literal::Integer(1))),
24425                };
24426                body.unions[rec].1.items.push(SelectItem::Expr {
24427                    expr: Expr::Array(alloc::vec![
24428                        Expr::Binary {
24429                            lhs: Box::new(parent_depth),
24430                            op: BinOp::Add,
24431                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24432                        },
24433                        rec_key,
24434                    ]),
24435                    alias: Some(srch.set_column.clone()),
24436                });
24437            }
24438            extra_cols.push(srch.set_column);
24439        }
24440
24441        if let Some(cyc) = cycle {
24442            let positions: Vec<usize> = cyc
24443                .columns
24444                .iter()
24445                .map(|c| pos_of(c))
24446                .collect::<Result<_, _>>()?;
24447            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24448            // cast it to text for the cycle path: membership only needs equality,
24449            // and the record text form gives SPG a TextArray path (SPG has no
24450            // typed record[] array). Cycle detection is unaffected.
24451            let base_row = Expr::Cast {
24452                expr: Box::new(row_of(&body.items, &positions)?),
24453                target: CastTarget::Text,
24454            };
24455            let rec_row = Expr::Cast {
24456                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24457                target: CastTarget::Text,
24458            };
24459            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24460            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24461            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24462            body.items.push(SelectItem::Expr {
24463                expr: Expr::Literal(dflt.clone()),
24464                alias: Some(cyc.mark_column.clone()),
24465            });
24466            body.items.push(SelectItem::Expr {
24467                expr: Expr::Array(alloc::vec![base_row]),
24468                alias: Some(cyc.path_column.clone()),
24469            });
24470            // rec mark: ROW(cols) already in the path → cycle.
24471            let hit = Expr::AnyAll {
24472                expr: Box::new(rec_row.clone()),
24473                op: BinOp::Eq,
24474                array: Box::new(col_ref(&cyc.path_column)),
24475                is_any: true,
24476            };
24477            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24478                Expr::Case {
24479                    operand: None,
24480                    branches: alloc::vec![(hit, Expr::Literal(mark))],
24481                    else_branch: Some(Box::new(Expr::Literal(dflt))),
24482                }
24483            } else {
24484                hit
24485            };
24486            body.unions[rec].1.items.push(SelectItem::Expr {
24487                expr: mark_expr,
24488                alias: Some(cyc.mark_column.clone()),
24489            });
24490            // rec path: array_append(cte.path, ROW(cols)).
24491            body.unions[rec].1.items.push(SelectItem::Expr {
24492                expr: Expr::FunctionCall {
24493                    name: "array_append".into(),
24494                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24495                },
24496                alias: Some(cyc.path_column.clone()),
24497            });
24498            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24499            let stop = Expr::Unary {
24500                op: UnOp::Not,
24501                expr: Box::new(col_ref(&cyc.mark_column)),
24502            };
24503            let w = &mut body.unions[rec].1.where_;
24504            *w = Some(match w.take() {
24505                Some(prev) => Expr::Binary {
24506                    lhs: Box::new(prev),
24507                    op: BinOp::And,
24508                    rhs: Box::new(stop),
24509                },
24510                None => stop,
24511            });
24512            extra_cols.push(cyc.mark_column);
24513            extra_cols.push(cyc.path_column);
24514        }
24515        cte.column_overrides.extend(extra_cols);
24516        Ok(())
24517    }
24518
24519    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24520    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24521    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24522        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24523            return Ok(None);
24524        }
24525        self.advance(); // SEARCH
24526        let depth_first = match self.peek() {
24527            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24528            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24529            other => {
24530                return Err(self.err(format!(
24531                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24532                )));
24533            }
24534        };
24535        self.advance();
24536        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24537            return Err(self.err(format!(
24538                "expected FIRST after SEARCH mode, got {:?}",
24539                self.peek()
24540            )));
24541        }
24542        self.advance();
24543        if !self.peek_is_by() {
24544            return Err(self.err(format!(
24545                "expected BY after SEARCH … FIRST, got {:?}",
24546                self.peek()
24547            )));
24548        }
24549        self.advance();
24550        let mut by_columns = alloc::vec![self.expect_ident_like()?];
24551        while matches!(self.peek(), Token::Comma) {
24552            self.advance();
24553            by_columns.push(self.expect_ident_like()?);
24554        }
24555        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24556            return Err(self.err(format!(
24557                "expected SET in SEARCH clause, got {:?}",
24558                self.peek()
24559            )));
24560        }
24561        self.advance();
24562        let set_column = self.expect_ident_like()?;
24563        Ok(Some(crate::ast::SearchClause {
24564            depth_first,
24565            by_columns,
24566            set_column,
24567        }))
24568    }
24569
24570    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24571    /// USING pathcol`. Returns None when the next token isn't CYCLE.
24572    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24573        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24574            return Ok(None);
24575        }
24576        self.advance(); // CYCLE
24577        let mut columns = alloc::vec![self.expect_ident_like()?];
24578        while matches!(self.peek(), Token::Comma) {
24579            self.advance();
24580            columns.push(self.expect_ident_like()?);
24581        }
24582        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24583            return Err(self.err(format!(
24584                "expected SET in CYCLE clause, got {:?}",
24585                self.peek()
24586            )));
24587        }
24588        self.advance();
24589        let mark_column = self.expect_ident_like()?;
24590        let mut mark_value = None;
24591        let mut default_value = None;
24592        if matches!(self.peek(), Token::To) {
24593            self.advance();
24594            mark_value = Some(self.parse_cycle_literal()?);
24595            if !matches!(self.peek(), Token::Default) {
24596                return Err(self.err(format!(
24597                    "expected DEFAULT after CYCLE … TO, got {:?}",
24598                    self.peek()
24599                )));
24600            }
24601            self.advance();
24602            default_value = Some(self.parse_cycle_literal()?);
24603        }
24604        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24605            return Err(self.err(format!(
24606                "expected USING in CYCLE clause, got {:?}",
24607                self.peek()
24608            )));
24609        }
24610        self.advance();
24611        let path_column = self.expect_ident_like()?;
24612        Ok(Some(crate::ast::CycleClause {
24613            columns,
24614            mark_column,
24615            mark_value,
24616            default_value,
24617            path_column,
24618        }))
24619    }
24620
24621    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24622    /// literal (string / bool / number) in PG.
24623    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24624        match self.parse_expr(0)? {
24625            Expr::Literal(l) => Ok(l),
24626            other => Err(self.err(format!(
24627                "CYCLE mark/default value must be a literal, got {other:?}"
24628            ))),
24629        }
24630    }
24631
24632    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24633        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24634        // Comes through as an identifier; consume it if present and
24635        // mark every CTE in the clause as recursive (PG semantics —
24636        // the flag is per-WITH, not per-CTE).
24637        let mut recursive = false;
24638        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24639            && s.eq_ignore_ascii_case("recursive")
24640        {
24641            self.advance();
24642            recursive = true;
24643        }
24644        let mut ctes = Vec::new();
24645        loop {
24646            let name = self.expect_ident_like()?;
24647            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24648            // PG uses these to rename the body's output columns; we
24649            // do the same below by overriding `columns[i].name`.
24650            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24651                self.advance();
24652                let mut names = Vec::new();
24653                loop {
24654                    names.push(self.expect_ident_like()?);
24655                    if matches!(self.peek(), Token::Comma) {
24656                        self.advance();
24657                        continue;
24658                    }
24659                    break;
24660                }
24661                if !matches!(self.peek(), Token::RParen) {
24662                    return Err(self.err(format!(
24663                        "expected ')' to close CTE column list, got {:?}",
24664                        self.peek()
24665                    )));
24666                }
24667                self.advance();
24668                names
24669            } else {
24670                Vec::new()
24671            };
24672            // AS is a reserved Token::As (used by SELECT-item / FROM
24673            // aliasing) — handle it specially rather than as a bare
24674            // ident.
24675            if !matches!(self.peek(), Token::As) {
24676                return Err(self.err(format!(
24677                    "expected AS after CTE name {name:?}, got {:?}",
24678                    self.peek()
24679                )));
24680            }
24681            self.advance();
24682            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24683            // MATERIALIZED` optimizer hints. SPG materialises every
24684            // CTE, so both spellings are accepted and absorbed.
24685            if matches!(self.peek(), Token::Not) {
24686                self.advance(); // NOT
24687                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24688                    if s.eq_ignore_ascii_case("materialized"))
24689                {
24690                    self.advance();
24691                } else {
24692                    return Err(self.err(format!(
24693                        "expected MATERIALIZED after AS NOT, got {:?}",
24694                        self.peek()
24695                    )));
24696                }
24697            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24698                if s.eq_ignore_ascii_case("materialized"))
24699            {
24700                self.advance();
24701            }
24702            if !matches!(self.peek(), Token::LParen) {
24703                return Err(self.err(format!(
24704                    "expected '(' after AS in WITH clause, got {:?}",
24705                    self.peek()
24706                )));
24707            }
24708            self.advance();
24709            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24710            // RETURNING) as the CTE body in addition to SELECT.
24711            // PG writable CTE semantics. UPDATE / DELETE come in as
24712            // bare Idents (lexer keeps SELECT / INSERT as reserved
24713            // tokens but treats the rest of DML as case-insensitive
24714            // idents).
24715            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24716            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24717            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24718            let body = match self.peek() {
24719                Token::Select => {
24720                    let inner = self.parse_select_stmt()?;
24721                    let Statement::Select(s) = inner else {
24722                        unreachable!("parse_select_stmt returns Select");
24723                    };
24724                    crate::ast::CteBody::Select(s)
24725                }
24726                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24727                // `SELECT * FROM t` this way and accepts it wherever a
24728                // SELECT goes, so the CTE body dispatch needs its own
24729                // arm: this match is keyed on the FIRST token, and
24730                // `Token::Table` fell through to a tail that then
24731                // rejected what it got. `parse_table_shorthand` has
24732                // returned a desugared SelectStatement since the
24733                // shorthand landed — only the routing was missing.
24734                // Round 868 found this by putting the shorthand in a
24735                // subquery; every earlier check used a top-level form.
24736                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24737                // `SELECT * FROM t` this way and accepts it wherever a
24738                // SELECT goes, so the CTE body dispatch needs its own
24739                // arm: this match is keyed on the FIRST token, and
24740                // `Token::Table` fell through to a tail that rejected
24741                // what it got. `parse_table_shorthand` has returned a
24742                // desugared SelectStatement since the shorthand landed —
24743                // only the routing was missing, here and in the derived
24744                // table's second-token gate. Round 868 found both by
24745                // putting the shorthand in a subquery; every earlier
24746                // check had used a top-level form.
24747                Token::Table
24748                    if matches!(
24749                        self.tokens.get(self.pos + 1),
24750                        Some(Token::Ident(_) | Token::QuotedIdent(_))
24751                    ) =>
24752                {
24753                    let mut head = self.parse_table_shorthand()?;
24754                    self.parse_setop_chain_into(&mut head)?;
24755                    self.parse_select_tail_into(&mut head)?;
24756                    crate::ast::CteBody::Select(head)
24757                }
24758                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24759                // WITH t(a) AS (VALUES (1), (2)) … lowers through
24760                // the shared rows helper onto a Select body.
24761                Token::Values => {
24762                    self.advance(); // VALUES
24763                    let mut head = self.parse_values_rows_body()?;
24764                    // A VALUES seed can head a set-operation chain —
24765                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24766                    // SELECT n+1 FROM t …). Attach any trailing
24767                    // UNION / INTERSECT / EXCEPT peers so the
24768                    // recursive-CTE body parses like the SELECT seed.
24769                    self.parse_setop_chain_into(&mut head)?;
24770                    crate::ast::CteBody::Select(head)
24771                }
24772                Token::Insert => {
24773                    let inner = self.parse_one_statement()?;
24774                    let Statement::Insert(s) = inner else {
24775                        unreachable!("Token::Insert routes to Insert");
24776                    };
24777                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24778                }
24779                _ if is_update_kw => {
24780                    let inner = self.parse_one_statement()?;
24781                    let Statement::Update(s) = inner else {
24782                        return Err(
24783                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24784                        );
24785                    };
24786                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24787                }
24788                _ if is_delete_kw => {
24789                    let inner = self.parse_one_statement()?;
24790                    let Statement::Delete(s) = inner else {
24791                        return Err(
24792                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24793                        );
24794                    };
24795                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24796                }
24797                // v7.39 (round 149) — PG 17 allows MERGE as a
24798                // data-modifying CTE body.
24799                _ if is_merge_kw => {
24800                    let inner = self.parse_one_statement()?;
24801                    let Statement::Merge(s) = inner else {
24802                        return Err(
24803                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24804                        );
24805                    };
24806                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24807                }
24808                // v7.39 (round 151) — a CTE body may itself be
24809                // WITH-headed (PG grammar: PreparableStmt carries its
24810                // own with_clause). The nested statement keeps its own
24811                // ctes; the modifying-CTE-at-top-level rule is enforced
24812                // at execution.
24813                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24814                    self.advance(); // WITH
24815                    match self.parse_with_cte_then_select()? {
24816                        Statement::Select(s) => crate::ast::CteBody::Select(s),
24817                        Statement::Insert(s) => {
24818                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24819                        }
24820                        Statement::Update(s) => {
24821                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24822                        }
24823                        Statement::Delete(s) => {
24824                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24825                        }
24826                        Statement::Merge(s) => {
24827                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24828                        }
24829
24830                        other => {
24831                            return Err(self.err(format!(
24832                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24833                            )));
24834                        }
24835                    }
24836                }
24837                other => {
24838                    return Err(self.err(format!(
24839                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24840                    )));
24841                }
24842            };
24843            if !matches!(self.peek(), Token::RParen) {
24844                return Err(self.err(format!(
24845                    "expected ')' after CTE body, got {:?}",
24846                    self.peek()
24847                )));
24848            }
24849            self.advance();
24850            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24851            // CTE, desugared into extra body columns by the engine.
24852            let search = self.parse_cte_search_clause()?;
24853            let cycle = self.parse_cte_cycle_clause()?;
24854            let mut cte = crate::ast::Cte {
24855                name,
24856                body,
24857                recursive,
24858                column_overrides,
24859                search,
24860                cycle,
24861            };
24862            self.validate_recursive_cte(&cte)?;
24863            self.desugar_cte_search_cycle(&mut cte)?;
24864            ctes.push(cte);
24865            if matches!(self.peek(), Token::Comma) {
24866                self.advance();
24867                continue;
24868            }
24869            break;
24870        }
24871        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24872        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24873        // the parsed CTEs to whichever statement the body produces.
24874        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24875        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24876        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24877        match self.peek() {
24878            Token::Select => {
24879                let body_stmt = self.parse_select_stmt()?;
24880                let Statement::Select(mut body) = body_stmt else {
24881                    unreachable!()
24882                };
24883                body.ctes = ctes;
24884                Ok(Statement::Select(body))
24885            }
24886            Token::Insert => {
24887                let body_stmt = self.parse_one_statement()?;
24888                let Statement::Insert(mut body) = body_stmt else {
24889                    unreachable!()
24890                };
24891                body.ctes = ctes;
24892                Ok(Statement::Insert(body))
24893            }
24894            _ if outer_is_update => {
24895                let body_stmt = self.parse_one_statement()?;
24896                let Statement::Update(mut body) = body_stmt else {
24897                    return Err(self.err(format!("expected UPDATE after WITH clause")));
24898                };
24899                body.ctes = ctes;
24900                Ok(Statement::Update(body))
24901            }
24902            _ if outer_is_delete => {
24903                let body_stmt = self.parse_one_statement()?;
24904                let Statement::Delete(mut body) = body_stmt else {
24905                    return Err(self.err(format!("expected DELETE after WITH clause")));
24906                };
24907                body.ctes = ctes;
24908                Ok(Statement::Delete(body))
24909            }
24910            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
24911            // WITH RECURSIVE is rejected with PG's exact message
24912            // (parse analysis, transformWithClause).
24913            _ if outer_is_merge => {
24914                if recursive {
24915                    return Err(self.err(String::from(
24916                        "WITH RECURSIVE is not supported for MERGE statement",
24917                    )));
24918                }
24919                let body_stmt = self.parse_one_statement()?;
24920                let Statement::Merge(mut body) = body_stmt else {
24921                    return Err(self.err(format!("expected MERGE after WITH clause")));
24922                };
24923                body.ctes = ctes;
24924                Ok(Statement::Merge(body))
24925            }
24926            other => Err(self.err(format!(
24927                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
24928            ))),
24929        }
24930    }
24931
24932    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
24933    /// already consumed the leading `EXISTS` ident via
24934    /// `self.advance()`.
24935    /// v7.13.0 — parse the rest of a `CASE … END` expression after
24936    /// the leading `CASE` ident has been consumed (mailrs round-5
24937    /// G9). Supports both the searched form
24938    /// (`CASE WHEN cond THEN val …`) and the simple form
24939    /// (`CASE operand WHEN val THEN val …`).
24940    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
24941        // Disambiguate searched vs simple form: if the next token
24942        // is `WHEN`, we're in the searched form. Otherwise the
24943        // intervening expression is the operand.
24944        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
24945            None
24946        } else {
24947            Some(Box::new(self.parse_expr(0)?))
24948        };
24949        let mut branches: Vec<(Expr, Expr)> = Vec::new();
24950        loop {
24951            match self.peek() {
24952                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
24953                    self.advance();
24954                    let cond = self.parse_expr(0)?;
24955                    match self.peek() {
24956                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
24957                            self.advance();
24958                        }
24959                        other => {
24960                            return Err(self.err(alloc::format!(
24961                                "expected THEN after CASE WHEN <expr>, got {other:?}"
24962                            )));
24963                        }
24964                    }
24965                    let value = self.parse_expr(0)?;
24966                    branches.push((cond, value));
24967                }
24968                _ => break,
24969            }
24970        }
24971        if branches.is_empty() {
24972            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
24973        }
24974        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
24975        {
24976            self.advance();
24977            Some(Box::new(self.parse_expr(0)?))
24978        } else {
24979            None
24980        };
24981        match self.peek() {
24982            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
24983                self.advance();
24984            }
24985            other => {
24986                return Err(self.err(alloc::format!(
24987                    "expected END to close CASE expression, got {other:?}"
24988                )));
24989            }
24990        }
24991        Ok(Expr::Case {
24992            operand,
24993            branches,
24994            else_branch,
24995        })
24996    }
24997
24998    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
24999    /// query-source position (EXISTS / IN / INSERT source / CTE body /
25000    /// view body). Caller consumed the WITH keyword. Only a SELECT
25001    /// outer is grammatical here; the data-modifying-CTE-at-top-level
25002    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
25003    /// maps correctly.
25004    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25005        let inner = self.parse_with_cte_then_select()?;
25006        match inner {
25007            Statement::Select(s) => Ok(s),
25008            other => Err(self.err(format!(
25009                "expected SELECT after WITH in a subquery, got {other:?}"
25010            ))),
25011        }
25012    }
25013
25014    /// True when the next token is the (unquoted) WITH keyword. WITH is
25015    /// reserved in PG, so a bare `with` can never be a column reference
25016    /// in these positions; a quoted `"with"` stays an identifier.
25017    fn peek_is_with_kw(&self) -> bool {
25018        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
25019    }
25020
25021    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
25022    /// `#[inline(never)]` keeps the large SelectStatement temporaries
25023    /// off parse_expr's recursive frame (the nesting-budget stack
25024    /// cliff — see the round-153 gate regression).
25025    #[inline(never)]
25026    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25027        if self.peek_is_with_kw() {
25028            self.advance();
25029            self.parse_nested_with_select()
25030        } else {
25031            match self.parse_select_stmt()? {
25032                Statement::Select(s) => Ok(s),
25033                other => Err(self.err(alloc::format!(
25034                    "expected SELECT inside ANY/ALL, got {other:?}"
25035                ))),
25036            }
25037        }
25038    }
25039
25040    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
25041        if !matches!(self.peek(), Token::LParen) {
25042            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
25043        }
25044        self.advance();
25045        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
25046        let s = if self.peek_is_with_kw() {
25047            self.advance();
25048            self.parse_nested_with_select()?
25049        } else {
25050            let inner = self.parse_select_stmt()?;
25051            let Statement::Select(s) = inner else {
25052                unreachable!("parse_select_stmt returns Select")
25053            };
25054            s
25055        };
25056        if !matches!(self.peek(), Token::RParen) {
25057            return Err(self.err(format!(
25058                "expected ')' after EXISTS-subquery, got {:?}",
25059                self.peek()
25060            )));
25061        }
25062        self.advance();
25063        Ok(Expr::Exists {
25064            subquery: Box::new(s),
25065            negated,
25066        })
25067    }
25068
25069    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
25070        self.advance(); // IN
25071        if !matches!(self.peek(), Token::LParen) {
25072            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
25073        }
25074        self.advance();
25075        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
25076        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
25077        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
25078            let s = if self.peek_is_with_kw() {
25079                self.advance();
25080                self.parse_nested_with_select()?
25081            } else {
25082                let inner = self.parse_select_stmt()?;
25083                let Statement::Select(s) = inner else {
25084                    unreachable!("parse_select_stmt always returns Statement::Select")
25085                };
25086                s
25087            };
25088            if !matches!(self.peek(), Token::RParen) {
25089                return Err(self.err(format!(
25090                    "expected ')' after IN-subquery, got {:?}",
25091                    self.peek()
25092                )));
25093            }
25094            self.advance();
25095            return Ok(Expr::InSubquery {
25096                expr: Box::new(expr),
25097                subquery: Box::new(s),
25098                negated,
25099            });
25100        }
25101        let mut elements = Vec::new();
25102        if !matches!(self.peek(), Token::RParen) {
25103            loop {
25104                elements.push(self.parse_expr(0)?);
25105                match self.peek() {
25106                    Token::Comma => {
25107                        self.advance();
25108                    }
25109                    Token::RParen => break,
25110                    other => {
25111                        return Err(
25112                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
25113                        );
25114                    }
25115                }
25116            }
25117        }
25118        self.advance(); // ')'
25119        // v7.30.2 (mailrs round-25) — flat InList node instead of a
25120        // left-deep OR-Eq chain: chain depth scaled with the element
25121        // count and overflowed the stack (eval + drop are recursive).
25122        if elements.is_empty() {
25123            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
25124        }
25125        Ok(Expr::InList {
25126            expr: Box::new(expr),
25127            list: elements,
25128            negated,
25129        })
25130    }
25131
25132    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
25133    /// already consumed by the caller. Elements must be numeric literals
25134    /// (with optional unary `-`); any compound expression is rejected at
25135    /// parse time so the runtime never needs to evaluate inside a vector.
25136    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
25137    /// has already consumed the `EXTRACT` token before calling us —
25138    /// we pick up at the opening `(`.
25139    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
25140    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
25141    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
25142    /// per-column OR-fold of
25143    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
25144    /// term)` so the existing FTS evaluator handles semantics.
25145    ///
25146    /// The mode modifier is accepted-and-ignored at v7.17 — all
25147    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
25148    /// mode operators (`+foo -bar`) would need their own parser
25149    /// (Phase 2.2c); customers who hit them today already get a
25150    /// correct lexeme-match against the bare term, only without
25151    /// the +/- precedence the customer asked for.
25152    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
25153        // Already at `MATCH`-consumed position; the dispatcher
25154        // confirmed the next token is `(`.
25155        if !matches!(self.peek(), Token::LParen) {
25156            return Err(self.err(alloc::format!(
25157                "expected '(' after MATCH, got {:?}",
25158                self.peek()
25159            )));
25160        }
25161        self.advance();
25162        let mut cols: Vec<Expr> = Vec::new();
25163        loop {
25164            cols.push(self.parse_expr(0)?);
25165            match self.peek() {
25166                Token::Comma => {
25167                    self.advance();
25168                }
25169                Token::RParen => break,
25170                other => {
25171                    return Err(self.err(alloc::format!(
25172                        "expected ',' or ')' in MATCH column list, got {other:?}"
25173                    )));
25174                }
25175            }
25176        }
25177        self.advance(); // ')'
25178        // Expect AGAINST.
25179        match self.peek() {
25180            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
25181                self.advance();
25182            }
25183            other => {
25184                return Err(self.err(alloc::format!(
25185                    "expected AGAINST after MATCH column list, got {other:?}"
25186                )));
25187            }
25188        }
25189        if !matches!(self.peek(), Token::LParen) {
25190            return Err(self.err(alloc::format!(
25191                "expected '(' after AGAINST, got {:?}",
25192                self.peek()
25193            )));
25194        }
25195        self.advance();
25196        // Read AGAINST's argument as a single primary token —
25197        // string literal, placeholder, or column-ref ident. We
25198        // can't call `parse_expr` / `parse_unary` here because
25199        // the postfix chain inside `parse_atom` would greedily
25200        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
25201        // and fail at "expected '(' after IN". Customers always
25202        // write a literal or bound parameter in AGAINST, so this
25203        // restriction is non-blocking; the error path explains
25204        // the limit if a more complex expression shows up.
25205        let term = match self.advance() {
25206            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
25207            Token::Placeholder(n) => Expr::Placeholder(n),
25208            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
25209                qualifier: None,
25210                name: s,
25211            }),
25212            other => {
25213                return Err(self.err(alloc::format!(
25214                    "MATCH ... AGAINST(<term>) expects a string literal, \
25215                     bound parameter, or column ref, got {other:?}"
25216                )));
25217            }
25218        };
25219        // Optional mode tail — accept-and-ignore at v7.17:
25220        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
25221        //   IN BOOLEAN MODE
25222        //   WITH QUERY EXPANSION
25223        loop {
25224            match self.peek() {
25225                // IN lexes as a reserved Token::In, not an ident,
25226                // so it gets its own arm.
25227                Token::In => {
25228                    self.advance();
25229                }
25230                Token::Ident(s) | Token::QuotedIdent(s)
25231                    if s.eq_ignore_ascii_case("natural")
25232                        || s.eq_ignore_ascii_case("language")
25233                        || s.eq_ignore_ascii_case("boolean")
25234                        || s.eq_ignore_ascii_case("mode")
25235                        || s.eq_ignore_ascii_case("with")
25236                        || s.eq_ignore_ascii_case("query")
25237                        || s.eq_ignore_ascii_case("expansion") =>
25238                {
25239                    self.advance();
25240                }
25241                _ => break,
25242            }
25243        }
25244        if !matches!(self.peek(), Token::RParen) {
25245            return Err(self.err(alloc::format!(
25246                "expected ')' to close AGAINST, got {:?}",
25247                self.peek()
25248            )));
25249        }
25250        self.advance();
25251        // Build per-column `to_tsvector('simple', col) @@
25252        // plainto_tsquery('simple', term)` and OR-fold.
25253        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
25254        let plainto = Expr::FunctionCall {
25255            name: String::from("plainto_tsquery"),
25256            args: alloc::vec![simple_lit(), term.clone()],
25257        };
25258        let mut folded: Option<Expr> = None;
25259        for col in cols {
25260            let to_tsv = Expr::FunctionCall {
25261                name: String::from("to_tsvector"),
25262                args: alloc::vec![simple_lit(), col],
25263            };
25264            let leaf = Expr::Binary {
25265                lhs: Box::new(to_tsv),
25266                op: crate::ast::BinOp::TsMatch,
25267                rhs: Box::new(plainto.clone()),
25268            };
25269            folded = Some(match folded {
25270                None => leaf,
25271                Some(prev) => Expr::Binary {
25272                    lhs: Box::new(prev),
25273                    op: crate::ast::BinOp::Or,
25274                    rhs: Box::new(leaf),
25275                },
25276            });
25277        }
25278        match folded {
25279            Some(e) => Ok(e),
25280            None => Err(self.err(String::from(
25281                "MATCH(...) AGAINST(...) requires at least one column",
25282            ))),
25283        }
25284    }
25285
25286    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
25287        if !matches!(self.peek(), Token::LParen) {
25288            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
25289        }
25290        self.advance();
25291        let field_name = self.expect_ident_like()?;
25292        let field = match field_name.to_ascii_lowercase().as_str() {
25293            // PG accepts the plural spellings (years/months/…/millenniums) as
25294            // aliases for the singular fields — its datetime unit table has both.
25295            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
25296            "year" | "years" => ExtractField::Year,
25297            "month" | "months" => ExtractField::Month,
25298            "day" | "days" => ExtractField::Day,
25299            "hour" | "hours" => ExtractField::Hour,
25300            "minute" | "minutes" => ExtractField::Minute,
25301            "second" | "seconds" => ExtractField::Second,
25302            "microsecond" | "microseconds" => ExtractField::Microsecond,
25303            "epoch" => ExtractField::Epoch,
25304            "dow" => ExtractField::Dow,
25305            "isodow" => ExtractField::Isodow,
25306            "doy" => ExtractField::Doy,
25307            "week" | "weeks" => ExtractField::Week,
25308            "isoyear" => ExtractField::Isoyear,
25309            "quarter" => ExtractField::Quarter,
25310            "decade" | "decades" => ExtractField::Decade,
25311            "century" | "centuries" => ExtractField::Century,
25312            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
25313            "julian" => ExtractField::Julian,
25314            "millisecond" | "milliseconds" => ExtractField::Millisecond,
25315            "timezone" => ExtractField::Timezone,
25316            "timezone_hour" => ExtractField::TimezoneHour,
25317            "timezone_minute" => ExtractField::TimezoneMinute,
25318            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
25319            // reports an unknown one with the source type (22023); carry the
25320            // raw name so eval can word it.
25321            other => ExtractField::Other(alloc::string::String::from(other)),
25322        };
25323        if !matches!(self.peek(), Token::From) {
25324            return Err(self.err(format!(
25325                "expected FROM after EXTRACT field, got {:?}",
25326                self.peek()
25327            )));
25328        }
25329        self.advance();
25330        let source = self.parse_expr(0)?;
25331        if !matches!(self.peek(), Token::RParen) {
25332            return Err(self.err(format!(
25333                "expected ')' to close EXTRACT, got {:?}",
25334                self.peek()
25335            )));
25336        }
25337        self.advance();
25338        Ok(Expr::Extract {
25339            field,
25340            source: Box::new(source),
25341        })
25342    }
25343
25344    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
25345    /// is already consumed; we expect a single string literal next and
25346    /// resolve it into `Literal::Interval` at parse time so the engine
25347    /// never has to re-tokenise inside the string.
25348    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
25349    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
25350    /// is the SQL-standard form and is left to the path below.
25351    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
25352        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
25353        let (offset, sign) = match self.peek() {
25354            Token::Minus => (1, "-"),
25355            _ => (0, ""),
25356        };
25357        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
25358            return None;
25359        };
25360        self.tokens
25361            .get(self.pos + offset + 1)
25362            .filter(|t| mysql_interval_unit(t).is_some())?;
25363        Some((alloc::format!("{sign}{n}"), offset + 1))
25364    }
25365
25366    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
25367    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
25368    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
25369    ///
25370    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
25371    /// this by parsing the group and then restoring `self.pos` — which could
25372    /// never have worked, because `advance()` DESTROYS the token it returns
25373    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
25374    /// inert only because both branches errored back then.
25375    fn interval_paren_is_quantity(&self) -> bool {
25376        let mut depth = 0usize;
25377        let mut saw_top_level_comma = false;
25378        let mut i = self.pos;
25379        while let Some(tok) = self.tokens.get(i) {
25380            match tok {
25381                Token::LParen => depth += 1,
25382                Token::RParen => {
25383                    depth = depth.saturating_sub(1);
25384                    if depth == 0 {
25385                        return !saw_top_level_comma
25386                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
25387                                .is_some();
25388                    }
25389                }
25390                // A comma directly inside the outermost parens means the
25391                // argument list of the INTERVAL() function.
25392                Token::Comma if depth == 1 => saw_top_level_comma = true,
25393                Token::Eof => return false,
25394                _ => {}
25395            }
25396            i += 1;
25397        }
25398        false
25399    }
25400
25401    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
25402        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
25403        // (the index of the last Ni ≤ N), distinct from the interval literal.
25404        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
25405        // is decided by a non-destructive lookahead (round 422) before either
25406        // branch consumes anything. MySQL only.
25407        if self.mysql_dialect
25408            && matches!(self.peek(), Token::LParen)
25409            && !self.interval_paren_is_quantity()
25410        {
25411            self.advance(); // (
25412            let mut args = Vec::new();
25413            if !matches!(self.peek(), Token::RParen) {
25414                loop {
25415                    args.push(self.parse_expr(0)?);
25416                    if matches!(self.peek(), Token::Comma) {
25417                        self.advance();
25418                        continue;
25419                    }
25420                    break;
25421                }
25422            }
25423            if !matches!(self.peek(), Token::RParen) {
25424                return Err(self.err(alloc::format!(
25425                    "expected ')' after INTERVAL() arguments, got {:?}",
25426                    self.peek()
25427                )));
25428            }
25429            self.advance(); // )
25430            return Ok(Expr::FunctionCall {
25431                name: alloc::string::String::from("interval"),
25432                args,
25433            });
25434        }
25435        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25436        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25437        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25438        // writes every date arithmetic there is, and it did not parse at
25439        // all. PG rejects the unquoted form outright (`syntax error at or
25440        // near "1"`, measured), so it is taken only in the MySQL dialect —
25441        // PG's own `INTERVAL '1' DAY` is untouched below.
25442        if self.mysql_dialect
25443            && let Some((text, consume)) = self.peek_unquoted_interval_count()
25444        {
25445            for _ in 0..consume {
25446                self.advance(); // the optional `-` and the number
25447            }
25448            let Some(unit) = mysql_interval_unit(self.peek()) else {
25449                return Err(self.err(alloc::format!(
25450                    "expected an interval unit after INTERVAL {text}, got {:?}",
25451                    self.peek()
25452                )));
25453            };
25454            self.advance(); // the unit
25455            let (months, days, micros) = scale_mysql_interval(&text, unit)
25456                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25457            return Ok(Expr::Literal(Literal::Interval {
25458                months,
25459                days,
25460                micros,
25461                // The canonical rendering, so Display round-trips into a
25462                // form both dialects read back.
25463                text: alloc::format!("{text} {unit}"),
25464            }));
25465        }
25466        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25467        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25468        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25469        // Those cannot fold into a compile-time `Literal::Interval`, so they
25470        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25471        // builtin, which builds the value at run time (and yields NULL for a
25472        // NULL quantity, as MariaDB does). The literal path above still folds
25473        // the constant case — it is cheaper and round-trips through Display.
25474        //
25475        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25476        // MySQL's quoted spelling) keep the qualifier path below.
25477        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25478            let qty = self.parse_expr(0)?;
25479            let Some(unit) = mysql_interval_unit(self.peek()) else {
25480                return Err(self.err(alloc::format!(
25481                    "expected an interval unit after INTERVAL <expr>, got {:?}",
25482                    self.peek()
25483                )));
25484            };
25485            self.advance(); // the unit
25486            return Ok(make_interval_call(qty, unit));
25487        }
25488        let tok = self.advance();
25489        let Token::String(text) = tok else {
25490            return Err(self.err(format!(
25491                "expected string literal after INTERVAL, got {tok:?}"
25492            )));
25493        };
25494        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25495        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25496        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25497        // bare number means and the leading/trailing precision.
25498        let field1 = interval_field_of(self.peek());
25499        let qualifier = if let Some(f1) = field1 {
25500            self.advance();
25501            let f2 = if matches!(self.peek(), Token::To) {
25502                self.advance();
25503                let Some(f) = interval_field_of(self.peek()) else {
25504                    return Err(self.err(format!(
25505                        "expected an interval field after TO, got {:?}",
25506                        self.peek()
25507                    )));
25508                };
25509                self.advance();
25510                Some(f)
25511            } else {
25512                None
25513            };
25514            Some((f1, f2))
25515        } else {
25516            None
25517        };
25518        let (months, days, micros) = match qualifier {
25519            Some(q) => interpret_qualified_interval(&text, q),
25520            None => parse_interval_text(&text),
25521        }
25522        .ok_or_else(|| ParseError {
25523            message: format!(
25524                "cannot parse INTERVAL {text:?}; \
25525                     expected `<n> <unit> [<n> <unit> ...]` with units \
25526                     microsecond[s], millisecond[s], second[s], minute[s], \
25527                     hour[s], day[s], week[s], month[s], year[s]"
25528            ),
25529            token_pos: self.consumed_pos(),
25530        })?;
25531        Ok(Expr::Literal(Literal::Interval {
25532            months,
25533            days,
25534            micros,
25535            text,
25536        }))
25537    }
25538
25539    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25540    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25541    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25542    /// than a pgvector literal.
25543    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25544        self.advance(); // consume `[`
25545        let mut items: Vec<Expr> = Vec::new();
25546        if !matches!(self.peek(), Token::RBracket) {
25547            loop {
25548                if matches!(self.peek(), Token::LBracket) {
25549                    items.push(self.parse_array_bracket_body()?);
25550                } else {
25551                    items.push(self.parse_expr(0)?);
25552                }
25553                match self.peek() {
25554                    Token::Comma => {
25555                        self.advance();
25556                    }
25557                    Token::RBracket => break,
25558                    other => {
25559                        return Err(self.err(alloc::format!(
25560                            "expected ',' or ']' in array literal, got {other:?}"
25561                        )));
25562                    }
25563                }
25564            }
25565        }
25566        self.advance(); // consume `]`
25567        Ok(Expr::Array(items))
25568    }
25569
25570    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25571        let mut elems = Vec::new();
25572        if matches!(self.peek(), Token::RBracket) {
25573            self.advance();
25574            return Ok(Expr::Literal(Literal::Vector(elems)));
25575        }
25576        loop {
25577            let e = self.parse_expr(0)?;
25578            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25579                message: format!("vector element must be a numeric literal, got {e:?}"),
25580                token_pos: self.pos,
25581            })?;
25582            elems.push(x);
25583            match self.peek() {
25584                Token::Comma => {
25585                    self.advance();
25586                }
25587                Token::RBracket => {
25588                    self.advance();
25589                    break;
25590                }
25591                other => {
25592                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25593                }
25594            }
25595        }
25596        Ok(Expr::Literal(Literal::Vector(elems)))
25597    }
25598
25599    /// Atom that started with an identifier: could be `t.col`, `col`, or
25600    /// `func(arg, ...)`. Detect each shape by looking at the next token.
25601    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25602    /// [, ...])`. Caller has already consumed `OVER`. Either clause
25603    /// is optional; an empty `()` is also legal (PG semantics).
25604    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25605    /// modifier between `name(args)` and `OVER (...)`. Default is
25606    /// `Respect`. Unrecognised idents leave the stream unchanged.
25607    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25608        let Token::Ident(s) = self.peek().clone() else {
25609            return NullTreatment::Respect;
25610        };
25611        let is_ignore = s.eq_ignore_ascii_case("ignore");
25612        let is_respect = s.eq_ignore_ascii_case("respect");
25613        if !is_ignore && !is_respect {
25614            return NullTreatment::Respect;
25615        }
25616        // Lookahead for NULLS — only consume both tokens together.
25617        // pos+1 must hold a "nulls" ident.
25618        if self.pos + 1 < self.tokens.len()
25619            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25620            && s2.eq_ignore_ascii_case("nulls")
25621        {
25622            self.advance();
25623            self.advance();
25624            return if is_ignore {
25625                NullTreatment::Ignore
25626            } else {
25627                NullTreatment::Respect
25628            };
25629        }
25630        NullTreatment::Respect
25631    }
25632
25633    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25634    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25635    /// (same shape as the `OVER` tail). Consumes the whole clause and
25636    /// returns the predicate; returns `None` when no `FILTER` follows.
25637    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25638        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25639            return Ok(None);
25640        };
25641        if !s.eq_ignore_ascii_case("filter") {
25642            return Ok(None);
25643        }
25644        self.advance(); // FILTER
25645        if !matches!(self.peek(), Token::LParen) {
25646            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25647        }
25648        self.advance(); // (
25649        if !matches!(self.peek(), Token::Where) {
25650            return Err(self.err(format!(
25651                "expected WHERE inside FILTER (...), got {:?}",
25652                self.peek()
25653            )));
25654        }
25655        self.advance(); // WHERE
25656        let cond = self.parse_expr(0)?;
25657        if !matches!(self.peek(), Token::RParen) {
25658            return Err(self.err(format!(
25659                "expected ')' to close FILTER (WHERE ...), got {:?}",
25660                self.peek()
25661            )));
25662        }
25663        self.advance(); // )
25664        Ok(Some(Box::new(cond)))
25665    }
25666
25667    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25668    /// the separator as the aggregate's second argument, which is the
25669    /// shape `string_agg` already takes. Returns whether one was there.
25670    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25671        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25672            return Ok(false);
25673        }
25674        self.advance();
25675        let Token::String(sep) = self.peek().clone() else {
25676            return Err(self.err(alloc::format!(
25677                "expected a string literal after SEPARATOR, got {:?}",
25678                self.peek()
25679            )));
25680        };
25681        self.advance();
25682        args.push(Expr::Literal(Literal::String(sep)));
25683        Ok(true)
25684    }
25685
25686    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25687    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25688    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25689    /// keys, or an empty vec when no `WITHIN GROUP` follows.
25690    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25691        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25692            return Ok(Vec::new());
25693        };
25694        if !s.eq_ignore_ascii_case("within") {
25695            return Ok(Vec::new());
25696        }
25697        self.advance(); // WITHIN
25698        if !matches!(self.peek(), Token::Group) {
25699            return Err(self.err(format!(
25700                "expected GROUP after WITHIN, got {:?}",
25701                self.peek()
25702            )));
25703        }
25704        self.advance(); // GROUP
25705        if !matches!(self.peek(), Token::LParen) {
25706            return Err(self.err(format!(
25707                "expected '(' after WITHIN GROUP, got {:?}",
25708                self.peek()
25709            )));
25710        }
25711        self.advance(); // (
25712        if !matches!(self.peek(), Token::Order) {
25713            return Err(self.err(format!(
25714                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25715                self.peek()
25716            )));
25717        }
25718        self.advance(); // ORDER
25719        if !self.peek_is_by() {
25720            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25721        }
25722        self.advance(); // BY
25723        let mut keys: Vec<OrderBy> = Vec::new();
25724        loop {
25725            // v7.39 (round 691) — save/restore, the discipline this parser
25726            // already uses around `pending_sample_preds`, so a subquery inside
25727            // a key neither inherits nor leaks the channel.
25728            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25729            let saved_coll = self.order_key_collation.take();
25730            let parsed = self.parse_expr(0);
25731            self.in_order_by_key = saved_flag;
25732            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25733            let expr = parsed?;
25734            let desc = if matches!(self.peek(), Token::Desc) {
25735                self.advance();
25736                true
25737            } else if matches!(self.peek(), Token::Asc) {
25738                self.advance();
25739                false
25740            } else {
25741                false
25742            };
25743            let nulls_first = self.parse_optional_nulls_placement()?;
25744            keys.push(OrderBy {
25745                expr,
25746                desc,
25747                nulls_first,
25748                collation,
25749            });
25750            if matches!(self.peek(), Token::Comma) {
25751                self.advance();
25752            } else {
25753                break;
25754            }
25755        }
25756        if !matches!(self.peek(), Token::RParen) {
25757            return Err(self.err(format!(
25758                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25759                self.peek()
25760            )));
25761        }
25762        self.advance(); // )
25763        Ok(keys)
25764    }
25765
25766    /// No frame clause is supported.
25767    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25768    fn parse_over_clause(
25769        &mut self,
25770    ) -> Result<
25771        (
25772            Vec<Expr>,
25773            Vec<(Expr, bool, Option<bool>)>,
25774            Option<WindowFrame>,
25775        ),
25776        ParseError,
25777    > {
25778        // `OVER w` — a named-window reference. The WINDOW clause
25779        // parses after the select list, so the name rides out as a
25780        // marker in partition_by; parse_bare_select substitutes the
25781        // definition once the clause is known.
25782        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25783            let name = w.clone();
25784            self.advance();
25785            return Ok((
25786                alloc::vec![Expr::Column(crate::ast::ColumnName {
25787                    qualifier: Some("__named_window__".to_string()),
25788                    name,
25789                })],
25790                Vec::new(),
25791                None,
25792            ));
25793        }
25794        if !matches!(self.peek(), Token::LParen) {
25795            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25796        }
25797        self.advance();
25798        let mut partition_by = Vec::new();
25799        let mut order_by = Vec::new();
25800        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25801        // window, refined in place. PG's rules (probed against 18.4) differ
25802        // from the bare `OVER w1` form, so the reference rides out under its
25803        // own marker and `substitute_named_windows` applies them. The base
25804        // name is any leading identifier that isn't a window-spec keyword.
25805        let base_window = match self.peek() {
25806            Token::Ident(s) | Token::QuotedIdent(s)
25807                if !s.eq_ignore_ascii_case("partition")
25808                    && !s.eq_ignore_ascii_case("rows")
25809                    && !s.eq_ignore_ascii_case("range")
25810                    && !s.eq_ignore_ascii_case("groups") =>
25811            {
25812                let n = s.clone();
25813                self.advance();
25814                Some(n)
25815            }
25816            _ => None,
25817        };
25818        // PARTITION BY ?
25819        // v7.37.6-B promoted PARTITION to a reserved keyword
25820        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25821        // `Token::Ident("partition")`. Accept both so older sources
25822        // and the new lexer surface land on the same path.
25823        let is_partition_kw = match self.peek() {
25824            Token::Partition => true,
25825            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25826            _ => false,
25827        };
25828        if is_partition_kw {
25829            self.advance();
25830            if !self.peek_is_by() {
25831                return Err(self.err(format!(
25832                    "expected BY after PARTITION, got {:?}",
25833                    self.peek()
25834                )));
25835            }
25836            self.advance();
25837            loop {
25838                partition_by.push(self.parse_expr(0)?);
25839                if matches!(self.peek(), Token::Comma) {
25840                    self.advance();
25841                    continue;
25842                }
25843                break;
25844            }
25845        }
25846        // ORDER BY ?
25847        if matches!(self.peek(), Token::Order) {
25848            self.advance();
25849            if !self.peek_is_by() {
25850                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25851            }
25852            self.advance();
25853            loop {
25854                let e = self.parse_expr(0)?;
25855                let desc = if matches!(self.peek(), Token::Desc) {
25856                    self.advance();
25857                    true
25858                } else if matches!(self.peek(), Token::Asc) {
25859                    self.advance();
25860                    false
25861                } else {
25862                    false
25863                };
25864                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25865                let nulls_first = self.parse_optional_nulls_placement()?;
25866                order_by.push((e, desc, nulls_first));
25867                if matches!(self.peek(), Token::Comma) {
25868                    self.advance();
25869                    continue;
25870                }
25871                break;
25872            }
25873        }
25874        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25875        // Both keywords come through the lexer as identifiers; match
25876        // case-insensitively.
25877        let mut frame: Option<WindowFrame> = None;
25878        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25879            let kind = if s.eq_ignore_ascii_case("rows") {
25880                Some(FrameKind::Rows)
25881            } else if s.eq_ignore_ascii_case("range") {
25882                Some(FrameKind::Range)
25883            } else if s.eq_ignore_ascii_case("groups") {
25884                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25885                Some(FrameKind::Groups)
25886            } else {
25887                None
25888            };
25889            if let Some(kind) = kind {
25890                self.advance();
25891                frame = Some(self.parse_frame_tail(kind)?);
25892            }
25893        }
25894        if !matches!(self.peek(), Token::RParen) {
25895            return Err(self.err(format!(
25896                "expected ')' to close OVER clause, got {:?}",
25897                self.peek()
25898            )));
25899        }
25900        self.advance();
25901        if let Some(base) = base_window {
25902            // A copy may refine but never override the base's partitioning
25903            // (PG rejects it outright, before looking the name up).
25904            if !partition_by.is_empty() {
25905                return Err(self.err(alloc::format!(
25906                    "cannot override PARTITION BY clause of window \"{base}\""
25907                )));
25908            }
25909            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
25910                qualifier: Some("__named_window_ref__".to_string()),
25911                name: base,
25912            })];
25913        }
25914        Ok((partition_by, order_by, frame))
25915    }
25916
25917    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
25918    /// or `RANGE` keyword was just consumed. Accepts both
25919    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
25920    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
25921    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
25922    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
25923        let (start, end) = if matches!(self.peek(), Token::Between) {
25924            self.advance();
25925            let start = self.parse_frame_bound()?;
25926            if !matches!(self.peek(), Token::And) {
25927                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
25928            }
25929            self.advance();
25930            let end = self.parse_frame_bound()?;
25931            (start, Some(end))
25932        } else {
25933            (self.parse_frame_bound()?, None)
25934        };
25935        let exclude = self.parse_frame_exclusion()?;
25936        Ok(WindowFrame {
25937            kind,
25938            start,
25939            end,
25940            exclude,
25941        })
25942    }
25943
25944    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
25945    /// after a frame spec. NO OTHERS is the default no-op.
25946    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
25947        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
25948            return Ok(FrameExclusion::NoOthers);
25949        }
25950        self.advance(); // EXCLUDE
25951        match self.peek() {
25952            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
25953                self.advance();
25954                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
25955                    return Err(self.err(format!(
25956                        "expected ROW after EXCLUDE CURRENT, got {:?}",
25957                        self.peek()
25958                    )));
25959                }
25960                self.advance();
25961                Ok(FrameExclusion::CurrentRow)
25962            }
25963            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
25964            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
25965            // Without this arm it fell to the catch-all, whose message
25966            // self-contradictingly listed GROUP as expected.
25967            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
25968                self.advance();
25969                Ok(FrameExclusion::Group)
25970            }
25971            Token::Group => {
25972                self.advance();
25973                Ok(FrameExclusion::Group)
25974            }
25975            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
25976                self.advance();
25977                Ok(FrameExclusion::Ties)
25978            }
25979            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
25980                self.advance();
25981                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
25982                    return Err(self.err(format!(
25983                        "expected OTHERS after EXCLUDE NO, got {:?}",
25984                        self.peek()
25985                    )));
25986                }
25987                self.advance();
25988                Ok(FrameExclusion::NoOthers)
25989            }
25990            other => Err(self.err(format!(
25991                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
25992            ))),
25993        }
25994    }
25995
25996    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
25997    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
25998    /// `UNBOUNDED FOLLOWING`.
25999    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
26000        // Interval-typed offset for a value-based RANGE frame over a
26001        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
26002        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
26003        // PRECEDING`.
26004        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
26005            let dir = self.expect_ident_like()?;
26006            return if dir.eq_ignore_ascii_case("preceding") {
26007                Ok(FrameBound::IntervalPreceding {
26008                    months,
26009                    days,
26010                    micros,
26011                })
26012            } else if dir.eq_ignore_ascii_case("following") {
26013                Ok(FrameBound::IntervalFollowing {
26014                    months,
26015                    days,
26016                    micros,
26017                })
26018            } else {
26019                Err(self.err(format!(
26020                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
26021                )))
26022            };
26023        }
26024        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
26025        if let Token::Integer(n) = *self.peek() {
26026            self.advance();
26027            let n: u64 = u64::try_from(n).map_err(|_| {
26028                self.err(format!(
26029                    "invalid frame offset {n} — expected non-negative integer"
26030                ))
26031            })?;
26032            let dir = self.expect_ident_like()?;
26033            return if dir.eq_ignore_ascii_case("preceding") {
26034                Ok(FrameBound::OffsetPreceding(n))
26035            } else if dir.eq_ignore_ascii_case("following") {
26036                Ok(FrameBound::OffsetFollowing(n))
26037            } else {
26038                Err(self.err(format!(
26039                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
26040                )))
26041            };
26042        }
26043        let first = self.expect_ident_like()?;
26044        if first.eq_ignore_ascii_case("unbounded") {
26045            let dir = self.expect_ident_like()?;
26046            return if dir.eq_ignore_ascii_case("preceding") {
26047                Ok(FrameBound::UnboundedPreceding)
26048            } else if dir.eq_ignore_ascii_case("following") {
26049                Ok(FrameBound::UnboundedFollowing)
26050            } else {
26051                Err(self.err(format!(
26052                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
26053                )))
26054            };
26055        }
26056        if first.eq_ignore_ascii_case("current") {
26057            let row = self.expect_ident_like()?;
26058            if !row.eq_ignore_ascii_case("row") {
26059                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
26060            }
26061            return Ok(FrameBound::CurrentRow);
26062        }
26063        Err(self.err(format!(
26064            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
26065        )))
26066    }
26067
26068    /// Detect and consume a leading interval offset in a frame bound —
26069    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
26070    /// `(months, days, micros)`. Leaves the cursor on the trailing
26071    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
26072    /// when the next tokens are not an interval offset.
26073    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
26074        // Shape A — `INTERVAL '1 day'`.
26075        if matches!(self.peek(), Token::Interval) {
26076            self.advance(); // INTERVAL
26077            let atom = self.parse_interval_atom()?;
26078            if let Expr::Literal(Literal::Interval {
26079                months,
26080                days,
26081                micros,
26082                ..
26083            }) = atom
26084            {
26085                return Ok(Some((months, days, micros)));
26086            }
26087            return Err(self.err("expected an interval literal in frame offset".to_string()));
26088        }
26089        // Shape B — `'1 day'::interval`. Look ahead for the exact
26090        // string / `::` / interval-target triple before committing.
26091        if let Token::String(text) = self.peek() {
26092            let target_is_interval = match self.tokens.get(self.pos + 2) {
26093                Some(Token::Interval) => true,
26094                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
26095                _ => false,
26096            };
26097            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
26098                && target_is_interval;
26099            if is_cast {
26100                let text = text.clone();
26101                self.advance(); // string
26102                self.advance(); // ::
26103                self.advance(); // interval
26104                let parts = parse_interval_text(&text).ok_or_else(|| {
26105                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
26106                })?;
26107                return Ok(Some(parts));
26108            }
26109        }
26110        Ok(None)
26111    }
26112
26113    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
26114        // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
26115        // `_binary'z'`. All three were `ERROR 1064 syntax error` here
26116        // and all three answer the literal on MySQL 9.7.2.
26117        //
26118        // It is not only syntax, which is why it waited for
26119        // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
26120        // because `_binary` makes the comparison byte-wise, while
26121        // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
26122        // dropping the charset would have turned a hard error into a
26123        // silently wrong comparison — worse than the error it replaced.
26124        //
26125        // An UNKNOWN charset is NOT an introducer: MySQL answers
26126        // `Unknown column '_nosuch'`, because it parses as a column
26127        // reference followed by a string. So the table decides, and it
26128        // is the same table `SET NAMES` reads.
26129        //
26130        // A space is allowed between the two (`_utf8mb4 'x'`), which
26131        // falls out of asking the token stream rather than the bytes.
26132        if self.mysql_dialect
26133            && let Token::String(_) = self.peek()
26134        {
26135            let lower = first.to_ascii_lowercase();
26136            let charset = if lower == "n" {
26137                // `N'…'` is the national character set, which MySQL
26138                // documents as utf8 — utf8mb3 in 9.7.2's spelling.
26139                //
26140                // utf8mb3 and utf8mb4 both fold case in their default
26141                // collations, so nothing SPG can be asked distinguishes
26142                // the two here: an ablation swapping this to utf8mb4
26143                // reddens no pin. Recorded rather than implied — the
26144                // spelling follows MySQL's documentation, not a
26145                // measurement.
26146                Some("utf8mb3")
26147            } else {
26148                // No filter here: the lookup below IS the test for
26149                // "is this a charset". An ablation that removed a filter
26150                // in this spot reddened nothing, which is how the two
26151                // were found to be one check written twice.
26152                lower.strip_prefix('_')
26153            };
26154            if let Some(cs) = charset
26155                && let Some(collation) = crate::charset::charset_default_collation(cs)
26156            {
26157                let Token::String(body) = self.advance() else {
26158                    unreachable!("peeked a string");
26159                };
26160                return Ok(Expr::Collate {
26161                    expr: Box::new(Expr::Literal(Literal::String(body))),
26162                    collation: String::from(collation),
26163                });
26164            }
26165        }
26166        if matches!(self.peek(), Token::Dot) {
26167            self.advance();
26168            let name = self.expect_ident_like()?;
26169            // v7.14.0 — schema-qualified function call
26170            // `<schema>.<fn>(args)`. PG dumps emit
26171            // `pg_catalog.set_config(...)` in the preamble. SPG
26172            // is single-namespace: drop the schema prefix and
26173            // route the dispatch on the bare function name.
26174            if matches!(self.peek(), Token::LParen) {
26175                return self.finish_ident_atom(name);
26176            }
26177            return Ok(Expr::Column(ColumnName {
26178                qualifier: Some(first),
26179                name,
26180            }));
26181        }
26182        if matches!(self.peek(), Token::LParen) {
26183            self.advance();
26184            // `COUNT(*)` — special-cased here because `*` isn't a normal
26185            // expression token. Lower-case match on `first` since the lexer
26186            // folds identifiers.
26187            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
26188                self.advance();
26189                if !matches!(self.peek(), Token::RParen) {
26190                    return Err(self.err(format!(
26191                        "expected ')' after COUNT(*), got {:?}",
26192                        self.peek()
26193                    )));
26194                }
26195                self.advance();
26196                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
26197                let filter = self.parse_filter_clause()?;
26198                // v4.12: COUNT(*) OVER (...) — same window tail.
26199                let null_treatment = self.parse_null_treatment_modifier();
26200                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26201                    && s.eq_ignore_ascii_case("over")
26202                {
26203                    self.advance();
26204                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
26205                    return Ok(Expr::WindowFunction {
26206                        name: "count_star".into(),
26207                        args: Vec::new(),
26208                        partition_by,
26209                        order_by,
26210                        frame,
26211                        null_treatment,
26212                        filter,
26213                    });
26214                }
26215                if let Some(filter) = filter {
26216                    return Ok(Expr::AggregateOrdered {
26217                        call: Box::new(Expr::FunctionCall {
26218                            name: "count_star".into(),
26219                            args: Vec::new(),
26220                        }),
26221                        order_by: Vec::new(),
26222                        distinct: false,
26223                        filter: Some(filter),
26224                    });
26225                }
26226                return Ok(Expr::FunctionCall {
26227                    name: "count_star".into(),
26228                    args: Vec::new(),
26229                });
26230            }
26231            // Function call. PG-style: zero-or-more comma-separated args.
26232            let mut args = Vec::new();
26233            // v7.38 (read01, T14) — named-argument notation `argname => value`.
26234            // Names are collected in lock-step with `args` and resolved to
26235            // positional order after the loop (the AST stays positional).
26236            let mut arg_names: Vec<Option<String>> = Vec::new();
26237            let mut agg_order_by: Vec<OrderBy> = Vec::new();
26238            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
26239            // seen, so the value arguments before it can be folded.
26240            let mut saw_separator = false;
26241            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
26242            // v7.32 (round-29) — accept the dual `ALL` quantifier too
26243            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
26244            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
26245                self.advance();
26246                true
26247            } else if matches!(self.peek(), Token::All) {
26248                self.advance();
26249                false
26250            } else {
26251                false
26252            };
26253            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
26254            // TIMESTAMPDIFF take a bare unit keyword as the first
26255            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
26256            // bare type keyword (DATE / TIME / DATETIME); lower them
26257            // onto string literals so the evaluator sees plain text.
26258            if ((first.eq_ignore_ascii_case("timestampadd")
26259                || first.eq_ignore_ascii_case("timestampdiff"))
26260                && matches!(self.peek(), Token::Ident(u) if matches!(
26261                    u.to_ascii_lowercase().as_str(),
26262                    "microsecond" | "second" | "minute" | "hour" | "day"
26263                        | "week" | "month" | "quarter" | "year"
26264                )))
26265                || (first.eq_ignore_ascii_case("get_format")
26266                    && matches!(self.peek(), Token::Ident(u) if matches!(
26267                        u.to_ascii_lowercase().as_str(),
26268                        "date" | "time" | "datetime" | "timestamp"
26269                    )))
26270            {
26271                if let Token::Ident(u) = self.peek() {
26272                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
26273                }
26274                self.advance();
26275                if matches!(self.peek(), Token::Comma) {
26276                    self.advance();
26277                }
26278            }
26279            // `ROW(a, b, …)` keyword constructor. Followed by a
26280            // comparison operator or [NOT] IN it joins the paren
26281            // row-constructor machinery (fieldwise parse-time
26282            // expansion); bare, it stays a `row` call the evaluator
26283            // renders as PG record text.
26284            if first.eq_ignore_ascii_case("row") {
26285                let mut row_items = Vec::new();
26286                if !matches!(self.peek(), Token::RParen) {
26287                    loop {
26288                        row_items.push(self.parse_expr(0)?);
26289                        match self.peek() {
26290                            Token::Comma => {
26291                                self.advance();
26292                            }
26293                            Token::RParen => break,
26294                            other => {
26295                                return Err(self.err(format!(
26296                                    "expected ',' or ')' in ROW(...), got {other:?}"
26297                                )));
26298                            }
26299                        }
26300                    }
26301                }
26302                self.advance(); // ')'
26303                let comparison_follows = matches!(
26304                    self.peek(),
26305                    Token::Eq
26306                        | Token::NotEq
26307                        | Token::Lt
26308                        | Token::LtEq
26309                        | Token::Gt
26310                        | Token::GtEq
26311                        | Token::In
26312                ) || (matches!(self.peek(), Token::Not)
26313                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
26314                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
26315                if comparison_follows && !row_items.is_empty() {
26316                    return self.parse_row_comparison_tail(row_items);
26317                }
26318                return Ok(Expr::FunctionCall {
26319                    name: String::from("row"),
26320                    args: row_items,
26321                });
26322            }
26323            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
26324            // the parse-mode keyword introduces the source text. SPG
26325            // carries XML as text, so both modes lower to __xmlparse(expr)
26326            // which validates well-formedness and returns Value::Xml.
26327            if first.eq_ignore_ascii_case("xmlparse")
26328                && matches!(self.peek(), Token::Ident(kw)
26329                    if kw.eq_ignore_ascii_case("document")
26330                        || kw.eq_ignore_ascii_case("content"))
26331            {
26332                let mode = match self.advance() {
26333                    Token::Ident(kw) => kw.to_ascii_lowercase(),
26334                    _ => unreachable!("peeked an ident"),
26335                };
26336                let src = self.parse_expr(0)?;
26337                if !matches!(self.peek(), Token::RParen) {
26338                    return Err(self.err(format!(
26339                        "expected ')' to close XMLPARSE, got {:?}",
26340                        self.peek()
26341                    )));
26342                }
26343                self.advance();
26344                return Ok(Expr::FunctionCall {
26345                    name: String::from("__xmlparse"),
26346                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
26347                });
26348            }
26349            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
26350            // keyword introduces the element name (a bare or quoted
26351            // identifier), then optional content expressions. Lower to a
26352            // plain `xmlelement(name_text, content …)` call.
26353            if first.eq_ignore_ascii_case("xmlelement")
26354                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
26355            {
26356                self.advance(); // consume NAME
26357                let elem_name = match self.peek().clone() {
26358                    Token::Ident(n) | Token::QuotedIdent(n) => {
26359                        self.advance();
26360                        n
26361                    }
26362                    other => {
26363                        return Err(self.err(format!(
26364                            "expected element name after XMLELEMENT NAME, got {other:?}"
26365                        )));
26366                    }
26367                };
26368                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
26369                while matches!(self.peek(), Token::Comma) {
26370                    self.advance();
26371                    args.push(self.parse_expr(0)?);
26372                }
26373                if !matches!(self.peek(), Token::RParen) {
26374                    return Err(self.err(format!(
26375                        "expected ')' to close XMLELEMENT, got {:?}",
26376                        self.peek()
26377                    )));
26378                }
26379                self.advance();
26380                return Ok(Expr::FunctionCall {
26381                    name: String::from("xmlelement"),
26382                    args,
26383                });
26384            }
26385            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
26386            // becomes a `<name>value</name>` element; a bare column infers its
26387            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
26388            // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
26389            // `CONVERT(expr USING cs)` was a syntax error at USING, and
26390            // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
26391            // `convert(bytea, src, dest)` and answered `column "char" does
26392            // not exist`. Both are casts in MySQL: measured on 9.7.2,
26393            // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
26394            // both 'A', and `CONVERT(123, CHAR)` is '123'.
26395            //
26396            // The charset is checked against the same table the introducers
26397            // use, so an unknown one is refused rather than quietly ignored.
26398            if self.mysql_dialect
26399                && first.eq_ignore_ascii_case("convert")
26400                && !matches!(self.peek(), Token::RParen)
26401            {
26402                let save = self.pos;
26403                let inner = self.parse_expr(0)?;
26404                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
26405                    self.advance();
26406                    let cs = match self.peek().clone() {
26407                        Token::Ident(n) | Token::QuotedIdent(n) => {
26408                            self.advance();
26409                            n
26410                        }
26411                        other => {
26412                            return Err(self.err(alloc::format!(
26413                                "expected a charset after USING, got {other:?}"
26414                            )));
26415                        }
26416                    };
26417                    let lc = cs.to_ascii_lowercase();
26418                    if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26419                        return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26420                    }
26421                    if !matches!(self.peek(), Token::RParen) {
26422                        return Err(self.err(alloc::format!(
26423                            "expected ')' after CONVERT … USING, got {:?}",
26424                            self.peek()
26425                        )));
26426                    }
26427                    self.advance();
26428                    let target = if lc == "binary" {
26429                        CastTarget::Named("binary".to_string())
26430                    } else {
26431                        CastTarget::Text
26432                    };
26433                    return self.finish_postfix_casts(Expr::Cast {
26434                        expr: alloc::boxed::Box::new(inner),
26435                        target,
26436                    });
26437                }
26438                if matches!(self.peek(), Token::Comma) {
26439                    self.advance();
26440                    // A type name here is MySQL's cast form; anything else
26441                    // (three string arguments) is PostgreSQL's `convert`,
26442                    // which keeps its own path.
26443                    if let Ok(target) = self.parse_cast_target()
26444                        && matches!(self.peek(), Token::RParen)
26445                    {
26446                        self.advance();
26447                        return self.finish_postfix_casts(Expr::Cast {
26448                            expr: alloc::boxed::Box::new(inner),
26449                            target,
26450                        });
26451                    }
26452                }
26453                self.pos = save;
26454            }
26455            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26456                let mut args: Vec<Expr> = Vec::new();
26457                loop {
26458                    let val = self.parse_expr(0)?;
26459                    let name = if matches!(self.peek(), Token::As) {
26460                        self.advance();
26461                        match self.peek().clone() {
26462                            Token::Ident(n) | Token::QuotedIdent(n) => {
26463                                self.advance();
26464                                n
26465                            }
26466                            other => {
26467                                return Err(self.err(format!(
26468                                    "expected name after AS in XMLFOREST, got {other:?}"
26469                                )));
26470                            }
26471                        }
26472                    } else if let Expr::Column(c) = &val {
26473                        c.name.clone()
26474                    } else {
26475                        return Err(
26476                            self.err("XMLFOREST element without a column name needs AS".into())
26477                        );
26478                    };
26479                    args.push(Expr::Literal(Literal::String(name)));
26480                    args.push(val);
26481                    if matches!(self.peek(), Token::Comma) {
26482                        self.advance();
26483                    } else {
26484                        break;
26485                    }
26486                }
26487                if !matches!(self.peek(), Token::RParen) {
26488                    return Err(self.err(format!(
26489                        "expected ')' to close XMLFOREST, got {:?}",
26490                        self.peek()
26491                    )));
26492                }
26493                self.advance();
26494                return Ok(Expr::FunctionCall {
26495                    name: String::from("xmlforest"),
26496                    args,
26497                });
26498            }
26499            // SQL-standard `POSITION(sub IN str)` — lowers onto
26500            // strpos(str, sub). IN is the argument separator here,
26501            // so the needle parses with the IN-tail suppressed.
26502            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26503                let saved = self.suppress_in_tail;
26504                self.suppress_in_tail = true;
26505                let needle = self.parse_expr(0);
26506                self.suppress_in_tail = saved;
26507                let needle = needle?;
26508                if matches!(self.peek(), Token::In) {
26509                    self.advance();
26510                    let haystack = self.parse_expr(0)?;
26511                    if !matches!(self.peek(), Token::RParen) {
26512                        return Err(self.err(format!(
26513                            "expected ')' to close POSITION, got {:?}",
26514                            self.peek()
26515                        )));
26516                    }
26517                    self.advance();
26518                    return Ok(Expr::FunctionCall {
26519                        name: String::from("strpos"),
26520                        args: alloc::vec![haystack, needle],
26521                    });
26522                }
26523                // position(sub, str) comma form (incl. bytea) —
26524                // hand the parsed first arg to the generic list.
26525                args.push(needle);
26526                if matches!(self.peek(), Token::Comma) {
26527                    self.advance();
26528                }
26529            }
26530            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26531            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26532            // plain comma forms TRIM(str) / TRIM(str, chars) keep
26533            // riding the generic argument list below.
26534            if first.eq_ignore_ascii_case("trim") {
26535                let mode = match self.peek() {
26536                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26537                        self.advance();
26538                        Some("btrim")
26539                    }
26540                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26541                        self.advance();
26542                        Some("ltrim")
26543                    }
26544                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26545                        self.advance();
26546                        Some("rtrim")
26547                    }
26548                    _ => None,
26549                };
26550                if mode.is_some() || matches!(self.peek(), Token::From) {
26551                    // TRIM([mode] FROM str) — no strip-chars.
26552                    let chars = if matches!(self.peek(), Token::From) {
26553                        None
26554                    } else {
26555                        Some(self.parse_expr(0)?)
26556                    };
26557                    if !matches!(self.peek(), Token::From) {
26558                        return Err(self.err(format!(
26559                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26560                            self.peek()
26561                        )));
26562                    }
26563                    self.advance();
26564                    let target = self.parse_expr(0)?;
26565                    if !matches!(self.peek(), Token::RParen) {
26566                        return Err(
26567                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26568                        );
26569                    }
26570                    self.advance();
26571                    let mut trim_args = alloc::vec![target];
26572                    if let Some(c) = chars {
26573                        trim_args.push(c);
26574                    }
26575                    return Ok(Expr::FunctionCall {
26576                        name: String::from(mode.unwrap_or("btrim")),
26577                        args: trim_args,
26578                    });
26579                }
26580            }
26581            if !matches!(self.peek(), Token::RParen) {
26582                loop {
26583                    // v7.38 (read01, T14) — `argname => value` names this arg.
26584                    // v7.39 (read01 round 77) — `argname := value` is the same
26585                    // thing, and it is the spelling PG's own docs lead with. It
26586                    // was simply never lexed here, so every `f(x := 1)` died in
26587                    // the parser regardless of what `f` was.
26588                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26589                        (
26590                            Token::Ident(n) | Token::QuotedIdent(n),
26591                            Some(Token::FatArrow | Token::ColonEq),
26592                        ) => {
26593                            let name = n.clone();
26594                            self.advance(); // name
26595                            self.advance(); // => / :=
26596                            Some(name)
26597                        }
26598                        _ => None,
26599                    };
26600                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26601                    // array's elements into a variadic call's trailing args
26602                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26603                    // reserved, so it arrives as a bare ident before the arg.
26604                    let is_variadic = this_name.is_none()
26605                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26606                    if is_variadic {
26607                        self.advance();
26608                    }
26609                    let arg = self.parse_expr(0)?;
26610                    args.push(match &this_name {
26611                        // The callee's parameter names decide the slot, and a
26612                        // user function's live in the catalog. Carry the name
26613                        // to eval rather than guessing here.
26614                        Some(n) => Expr::NamedArg {
26615                            name: n.clone(),
26616                            expr: Box::new(arg),
26617                        },
26618                        None if is_variadic => Expr::Variadic(Box::new(arg)),
26619                        None => arg,
26620                    });
26621                    arg_names.push(this_name);
26622                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
26623                    // The `::` cast already worked; this lowers the
26624                    // function form onto the same Expr::Cast node.
26625                    if first.eq_ignore_ascii_case("cast")
26626                        && args.len() == 1
26627                        && matches!(self.peek(), Token::As)
26628                    {
26629                        self.advance();
26630                        let target = self.parse_cast_target()?;
26631                        if !matches!(self.peek(), Token::RParen) {
26632                            return Err(self.err(format!(
26633                                "expected ')' to close CAST, got {:?}",
26634                                self.peek()
26635                            )));
26636                        }
26637                        self.advance();
26638                        return Ok(Expr::Cast {
26639                            expr: Box::new(args.pop().expect("one arg")),
26640                            target,
26641                        });
26642                    }
26643                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26644                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26645                    // keywords; SPG's lexer makes them plain idents (so they'd be
26646                    // read as column refs). Lower the keyword to the string form
26647                    // the evaluator already accepts.
26648                    if first.eq_ignore_ascii_case("normalize")
26649                        && args.len() == 1
26650                        && matches!(self.peek(), Token::Comma)
26651                    {
26652                        let form = match self.tokens.get(self.pos + 1) {
26653                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26654                                let up = f.to_ascii_uppercase();
26655                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26656                            }
26657                            _ => None,
26658                        };
26659                        if let Some(up) = form {
26660                            self.advance(); // comma
26661                            self.advance(); // form keyword
26662                            args.push(Expr::Literal(Literal::String(up)));
26663                        }
26664                    }
26665                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26666                    // form. Desugars to the comma-list shape evaluator already
26667                    // handles. Triggered after the first arg when the function
26668                    // name is substring / substr and the next token is FROM
26669                    // (a reserved keyword in PG; SPG also reserves it).
26670                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26671                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26672                    // internal __substring_similar(str, pat, esc) call.
26673                    if (first.eq_ignore_ascii_case("substring")
26674                        || first.eq_ignore_ascii_case("substr"))
26675                        && args.len() == 1
26676                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26677                    {
26678                        self.advance(); // SIMILAR
26679                        let pattern = self.parse_expr(0)?;
26680                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26681                        {
26682                            return Err(self.err(format!(
26683                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26684                                self.peek()
26685                            )));
26686                        }
26687                        self.advance(); // ESCAPE
26688                        let esc = self.parse_expr(0)?;
26689                        if !matches!(self.peek(), Token::RParen) {
26690                            return Err(self.err(format!(
26691                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
26692                                self.peek()
26693                            )));
26694                        }
26695                        self.advance();
26696                        args.push(pattern);
26697                        args.push(esc);
26698                        return Ok(Expr::FunctionCall {
26699                            name: "__substring_similar".to_string(),
26700                            args,
26701                        });
26702                    }
26703                    if (first.eq_ignore_ascii_case("substring")
26704                        || first.eq_ignore_ascii_case("substr"))
26705                        && args.len() == 1
26706                        && matches!(self.peek(), Token::From | Token::For)
26707                    {
26708                        // `substring(str FROM pos [FOR len])`, or the FOR-only
26709                        // `substring(str FOR len)` which PG treats as FROM 1.
26710                        if matches!(self.peek(), Token::From) {
26711                            self.advance();
26712                            let start = self.parse_expr(0)?;
26713                            args.push(start);
26714                        } else {
26715                            args.push(Expr::Literal(Literal::Integer(1)));
26716                        }
26717                        if matches!(self.peek(), Token::For) {
26718                            self.advance();
26719                            let length = self.parse_expr(0)?;
26720                            args.push(length);
26721                        }
26722                        if !matches!(self.peek(), Token::RParen) {
26723                            return Err(self.err(format!(
26724                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26725                                self.peek()
26726                            )));
26727                        }
26728                        self.advance();
26729                        return Ok(Expr::FunctionCall {
26730                            name: first.to_ascii_lowercase(),
26731                            args,
26732                        });
26733                    }
26734                    // PG `overlay(str PLACING repl FROM n [FOR len])`
26735                    // syntactic form. Desugars to the `overlay(str,
26736                    // repl, n[, len])` comma-list shape the evaluator
26737                    // already implements. `PLACING` is not a reserved
26738                    // token in SPG, so it arrives as a bare Ident.
26739                    if first.eq_ignore_ascii_case("overlay")
26740                        && args.len() == 1
26741                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26742                    {
26743                        self.advance(); // consume PLACING
26744                        args.push(self.parse_expr(0)?); // replacement
26745                        if !matches!(self.peek(), Token::From) {
26746                            return Err(self.err(format!(
26747                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26748                                self.peek()
26749                            )));
26750                        }
26751                        self.advance();
26752                        args.push(self.parse_expr(0)?); // start position
26753                        if matches!(self.peek(), Token::For) {
26754                            self.advance();
26755                            args.push(self.parse_expr(0)?); // length
26756                        }
26757                        if !matches!(self.peek(), Token::RParen) {
26758                            return Err(self.err(format!(
26759                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26760                                self.peek()
26761                            )));
26762                        }
26763                        self.advance();
26764                        return Ok(Expr::FunctionCall {
26765                            name: String::from("overlay"),
26766                            args,
26767                        });
26768                    }
26769                    // `TRIM(chars FROM str)` — the keyword-less
26770                    // spelling lands here after the chars parse
26771                    // (the keyword forms return earlier).
26772                    if first.eq_ignore_ascii_case("trim")
26773                        && args.len() == 1
26774                        && matches!(self.peek(), Token::From)
26775                    {
26776                        self.advance();
26777                        let target = self.parse_expr(0)?;
26778                        if !matches!(self.peek(), Token::RParen) {
26779                            return Err(self.err(format!(
26780                                "expected ')' to close TRIM(chars FROM str), got {:?}",
26781                                self.peek()
26782                            )));
26783                        }
26784                        self.advance();
26785                        let chars = args.pop().expect("one arg");
26786                        return Ok(Expr::FunctionCall {
26787                            name: String::from("btrim"),
26788                            args: alloc::vec![target, chars],
26789                        });
26790                    }
26791                    // v7.24 (round-16 A) — aggregate-internal
26792                    // ordering: `array_agg(x ORDER BY y DESC NULLS
26793                    // LAST)`. Keys close the argument list.
26794                    if matches!(self.peek(), Token::Order) {
26795                        self.advance();
26796                        if !self.peek_is_by() {
26797                            return Err(self.err(format!(
26798                                "expected BY after ORDER in aggregate args, got {:?}",
26799                                self.peek()
26800                            )));
26801                        }
26802                        self.advance();
26803                        loop {
26804                            // v7.39 (round 691) — save/restore, the discipline this parser
26805                            // already uses around `pending_sample_preds`, so a subquery inside
26806                            // a key neither inherits nor leaks the channel.
26807                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26808                            let saved_coll = self.order_key_collation.take();
26809                            let parsed = self.parse_expr(0);
26810                            self.in_order_by_key = saved_flag;
26811                            let collation =
26812                                core::mem::replace(&mut self.order_key_collation, saved_coll);
26813                            let expr = parsed?;
26814                            let desc = if matches!(self.peek(), Token::Desc) {
26815                                self.advance();
26816                                true
26817                            } else if matches!(self.peek(), Token::Asc) {
26818                                self.advance();
26819                                false
26820                            } else {
26821                                false
26822                            };
26823                            let nulls_first = self.parse_optional_nulls_placement()?;
26824                            agg_order_by.push(OrderBy {
26825                                expr,
26826                                desc,
26827                                nulls_first,
26828                                collation,
26829                            });
26830                            if matches!(self.peek(), Token::Comma) {
26831                                self.advance();
26832                            } else {
26833                                break;
26834                            }
26835                        }
26836                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26837                        // follow the ORDER BY inside GROUP_CONCAT.
26838                        if self.consume_group_concat_separator(&mut args)? {
26839                            saw_separator = true;
26840                        }
26841                        if !matches!(self.peek(), Token::RParen) {
26842                            return Err(self.err(format!(
26843                                "expected ')' after aggregate ORDER BY, got {:?}",
26844                                self.peek()
26845                            )));
26846                        }
26847                        break;
26848                    }
26849                    // v7.39 (round 354, M12) — …or directly after the
26850                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26851                    // own spelling of what PG passes as string_agg's second
26852                    // argument; it was a parse error, so every MySQL query
26853                    // that names its own separator failed outright.
26854                    if self.consume_group_concat_separator(&mut args)? {
26855                        saw_separator = true;
26856                        break;
26857                    }
26858                    match self.peek() {
26859                        Token::Comma => {
26860                            self.advance();
26861                        }
26862                        Token::RParen => break,
26863                        other => {
26864                            return Err(self.err(format!(
26865                                "expected ',' or ')' in function args, got {other:?}"
26866                            )));
26867                        }
26868                    }
26869                }
26870            }
26871            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26872            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26873            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26874            // meaning a separator — that is what the explicit SEPARATOR
26875            // tail is for. Fold them into one `concat(...)` so the
26876            // aggregate keeps its single value argument.
26877            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26878                let values = args.len() - usize::from(saw_separator);
26879                if values > 1 {
26880                    let sep_arg = if saw_separator { args.pop() } else { None };
26881                    let folded = Expr::FunctionCall {
26882                        name: "concat".to_string(),
26883                        args: core::mem::take(&mut args),
26884                    };
26885                    args.push(folded);
26886                    if let Some(sep) = sep_arg {
26887                        args.push(sep);
26888                    }
26889                }
26890            }
26891            self.advance(); // consume ')'
26892            // v7.39 (read01 round 77) — named arguments are NOT reordered here
26893            // any more. The parser has no catalog, so it could only ever resolve
26894            // the handful of `make_*` builtins whose parameter names were baked
26895            // into a table right here — every user function got
26896            // "does not support named arguments", though the catalog has been
26897            // storing its parameter names all along. Reordering happens in eval,
26898            // in one place, for builtins and user functions alike.
26899            // v7.32 (round-29) — ordered-set aggregate tail
26900            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
26901            // (percentile_cont / percentile_disc / mode). The sort spec
26902            // lands in the same `order_by` slot a decorated aggregate
26903            // uses; the executor dispatches on the function name. WITHIN
26904            // GROUP and an intra-argument ORDER BY are mutually
26905            // exclusive (PG rejects both).
26906            let within_group_order = self.parse_within_group_clause()?;
26907            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
26908                return Err(self.err(
26909                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
26910                        .into(),
26911                ));
26912            }
26913            let within_group_seen = !within_group_order.is_empty();
26914            let agg_order_by = if within_group_order.is_empty() {
26915                agg_order_by
26916            } else {
26917                within_group_order
26918            };
26919            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
26920            let filter = self.parse_filter_clause()?;
26921            // v4.12: window-function tail — `name(args) OVER (...)`.
26922            // Promotes the just-parsed FunctionCall into a
26923            // WindowFunction node carrying partition + order.
26924            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
26925            // / `RESPECT NULLS OVER (...)` between the closing paren
26926            // and `OVER`.
26927            let null_treatment = self.parse_null_treatment_modifier();
26928            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26929                && s.eq_ignore_ascii_case("over")
26930            {
26931                self.advance();
26932                // v7.39 (round 230) — PG implements neither modifier for a
26933                // windowed call and says so (0A000). Both used to be parsed
26934                // and then silently dropped here, so `count(DISTINCT v)
26935                // OVER (…)` quietly answered the non-distinct count.
26936                if agg_distinct {
26937                    return Err(
26938                        self.err("DISTINCT is not implemented for window functions".to_string())
26939                    );
26940                }
26941                if !agg_order_by.is_empty() {
26942                    // PG separates the two shapes that land here: a
26943                    // WITHIN GROUP call is an ordered-set aggregate and gets
26944                    // its own message naming the aggregate; a plain
26945                    // `agg(x ORDER BY y)` gets the generic one.
26946                    let msg = if within_group_seen {
26947                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
26948                    } else {
26949                        "aggregate ORDER BY is not implemented for window functions".to_string()
26950                    };
26951                    return Err(self.err(msg));
26952                }
26953                let (partition_by, order_by, frame) = self.parse_over_clause()?;
26954                return Ok(Expr::WindowFunction {
26955                    name: first,
26956                    args,
26957                    partition_by,
26958                    order_by,
26959                    frame,
26960                    null_treatment,
26961                    filter,
26962                });
26963            }
26964            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
26965                return Ok(Expr::AggregateOrdered {
26966                    call: Box::new(Expr::FunctionCall { name: first, args }),
26967                    order_by: agg_order_by,
26968                    distinct: agg_distinct,
26969                    filter,
26970                });
26971            }
26972            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
26973            // over TIMESTAMPTZ and has no timestamp overload, so a
26974            // timestamp argument is coerced on the way in and the answer
26975            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
26976            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
26977            // zone`. SPG answered `timestamp without time zone`, dropping
26978            // the offset from every rendering.
26979            //
26980            // Writing the coercion PG performs makes the existing
26981            // argument-driven typing (the one `date_trunc` uses) reach the
26982            // right answer, rather than teaching the type layer a second
26983            // rule. MySQL's DATE_ADD is a different function that returns
26984            // DATE or DATETIME, so this is PG-dialect only.
26985            //
26986            // Out-of-line because this sits on the RECURSIVE descent
26987            // frame: an inline block with locals here costs every nesting
26988            // level, and the suite's deep-nesting sentinel overflowed the
26989            // 512 KiB parser stack the moment one was added (round 430's
26990            // lesson, in the same shape).
26991            if !self.mysql_dialect {
26992                lift_date_add_arg_to_timestamptz(&first, &mut args);
26993            }
26994            return Ok(Expr::FunctionCall { name: first, args });
26995        }
26996        // v7.9.20 — SQL-standard parenless keyword expressions
26997        // (PG treats these as functions called without parens).
26998        // Resolve to a synthetic FunctionCall so the engine's
26999        // eval path reuses the existing function-call routing.
27000        // mailrs G3.
27001        let lc = first.to_ascii_lowercase();
27002        if matches!(
27003            lc.as_str(),
27004            "current_date"
27005                | "current_time"
27006                | "current_timestamp"
27007                | "localtimestamp"
27008                | "localtime"
27009                // v7.37.17 (17.6 siblings) — session-identity SQL-
27010                // standard parenless keywords. current_user /
27011                // session_user / user were already caught by the
27012                // pgwire canned-response shortcut but bare-select
27013                // in the embedded engine went through Expr::Column
27014                // and errored. Adding them here so the parser
27015                // resolves to a synthetic FunctionCall that reuses
27016                // the existing eval/functions.rs dispatch.
27017                | "current_user"
27018                | "session_user"
27019                | "current_role"
27020                | "current_catalog"
27021                | "current_schema"
27022                | "current_database"
27023                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
27024                | "system_user"
27025        ) {
27026            return Ok(Expr::FunctionCall {
27027                name: lc,
27028                args: Vec::new(),
27029            });
27030        }
27031        Ok(Expr::Column(ColumnName {
27032            qualifier: None,
27033            name: first,
27034        }))
27035    }
27036}
27037
27038/// v7.39 (round 522) — write the coercion PG's `date_add` /
27039/// `date_subtract` signature performs.
27040///
27041/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
27042/// timestamp argument is cast on the way in and the answer is
27043/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
27044/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
27045/// `timestamp without time zone`, dropping the offset from every
27046/// rendering of the result.
27047///
27048/// Writing the cast the signature implies lets the existing
27049/// argument-driven typing (the one `date_trunc` uses) reach the right
27050/// answer instead of teaching the type layer a second rule. MySQL's
27051/// DATE_ADD is a different function returning DATE or DATETIME, so the
27052/// caller applies this in PG dialect only.
27053///
27054/// A free function, and not a block at the call site, because the caller
27055/// is on the recursive-descent frame chain.
27056#[inline(never)]
27057fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
27058    if args.len() != 2
27059        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
27060    {
27061        return;
27062    }
27063    let base = args.remove(0);
27064    args.insert(
27065        0,
27066        Expr::Cast {
27067            expr: Box::new(base),
27068            target: CastTarget::Timestamptz,
27069        },
27070    );
27071}
27072
27073/// v6.8.2 — walk an expression tree and return the first column
27074/// reference's bare name. Used by `parse_create_index_stmt_after_create`
27075/// to derive `CreateIndexStatement.column` from an expression
27076/// key (so downstream planner code resolving a primary column
27077/// position keeps working with expression indexes). Returns
27078/// `None` when the expression has no column ref at all — caller
27079/// surfaces that as a parse error.
27080fn extract_first_column(expr: &Expr) -> Option<String> {
27081    match expr {
27082        Expr::Column(cn) => Some(cn.name.clone()),
27083        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
27084        Expr::Binary { lhs, rhs, .. } => {
27085            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
27086        }
27087        Expr::Unary { expr: e, .. } => extract_first_column(e),
27088        // v7.39 (read01 round 93) — a cast wraps its operand: a common
27089        // expression-index key is `lower(col::text)`, where the column
27090        // sits under the `::text` cast inside the function arg. Without
27091        // descending here the key was rejected as "references no column".
27092        Expr::Cast { expr: e, .. } => extract_first_column(e),
27093        // v7.39.2 — and a COLLATE wraps its operand the same way.
27094        // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
27095        // column the moment the clause became a node instead of being
27096        // absorbed, and the key was rejected as referencing none. This
27097        // is the shape the wildcard below silently produces, which is
27098        // why it is spelled out.
27099        Expr::Collate { expr: e, .. } => extract_first_column(e),
27100        _ => None,
27101    }
27102}
27103
27104fn maybe_not(expr: Expr, negated: bool) -> Expr {
27105    if negated {
27106        Expr::Unary {
27107            op: UnOp::Not,
27108            expr: Box::new(expr),
27109        }
27110    } else {
27111        expr
27112    }
27113}
27114
27115/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
27116/// things in the two dialects, and SPG read all three PG's way:
27117///
27118/// | token | PG (and SPG) | MySQL, measured |
27119/// |---|---|---|
27120/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
27121/// | `&&` | inet / array overlap | **AND** |
27122/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
27123///
27124/// `1 || 0` answering the string '10' on a MySQL session is a wrong
27125/// answer with no error, which is why they are routed here rather than
27126/// left to the shared table.
27127impl Parser {
27128    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
27129        if self.mysql_dialect {
27130            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
27131            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
27132            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
27133            if let Token::Ident(w) = tok
27134                && w.eq_ignore_ascii_case("div")
27135            {
27136                return Some((BinOp::IntDiv, 8));
27137            }
27138            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
27139            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
27140            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
27141            // there sits in operand position, not infix).
27142            if let Token::Ident(w) = tok
27143                && w.eq_ignore_ascii_case("mod")
27144            {
27145                return Some((BinOp::Mod, 8));
27146            }
27147            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
27148            // plain ident to the lexer. Its precedence sits between OR (1)
27149            // and AND (3) — hence rung 2, the slot freed by moving AND up.
27150            if let Token::Ident(w) = tok
27151                && w.eq_ignore_ascii_case("xor")
27152            {
27153                return Some((BinOp::LogicalXor, 2));
27154            }
27155            match tok {
27156                Token::Concat => return Some((BinOp::Or, 1)),
27157                // MySQL's `&&` is logical AND, sharing AND's rung (3).
27158                Token::InetOverlap => return Some((BinOp::And, 3)),
27159                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
27160                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
27161                _ => {}
27162            }
27163        }
27164        binop_from(tok)
27165    }
27166}
27167
27168// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
27169// (which sits strictly between OR and AND), every level from AND upward was
27170// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
27171// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
27172// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
27173// the *relative* order of every PG operator is unchanged by the shift.
27174fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
27175    let pair = match tok {
27176        Token::Or => (BinOp::Or, 1),
27177        Token::And => (BinOp::And, 3),
27178        Token::Eq => (BinOp::Eq, 5),
27179        Token::NotEq => (BinOp::NotEq, 5),
27180        Token::Lt => (BinOp::Lt, 5),
27181        Token::LtEq => (BinOp::LtEq, 5),
27182        Token::Gt => (BinOp::Gt, 5),
27183        Token::GtEq => (BinOp::GtEq, 5),
27184        // pgvector distance ops all sit on the same rung — tighter than
27185        // comparisons (5) so `col <-> v < threshold` parses correctly.
27186        Token::L2Distance => (BinOp::L2Distance, 6),
27187        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
27188        // comparison rung.
27189        Token::GeomParallel => (BinOp::GeomParallel, 5),
27190        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
27191        // comparison rung.
27192        Token::OverLeft => (BinOp::OverLeft, 5),
27193        Token::OverRight => (BinOp::OverRight, 5),
27194        Token::GeomPerp => (BinOp::GeomPerp, 5),
27195        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
27196        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
27197        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
27198        Token::InnerProduct => (BinOp::InnerProduct, 6),
27199        Token::CosineDistance => (BinOp::CosineDistance, 6),
27200        Token::Plus => (BinOp::Add, 7),
27201        Token::Minus => (BinOp::Sub, 7),
27202        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
27203        // binds every "other" operator (`||`, `|`, `&`, `#`, the
27204        // pgvector distances above) BETWEEN additive (7) and the
27205        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
27206        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
27207        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
27208        // ("matches PG conceptually" — the round-753 audit measured it
27209        // false; the old rung errored on `'a' || 1 + 1` with
27210        // `text + integer`). Same-level chains left-fold, as PG does.
27211        Token::Concat => (BinOp::Concat, 6),
27212        Token::Pipe => (BinOp::BitOr, 6),
27213        Token::Amp => (BinOp::BitAnd, 6),
27214        Token::Star => (BinOp::Mul, 8),
27215        Token::Slash => (BinOp::Div, 8),
27216        Token::Percent => (BinOp::Mod, 8),
27217        // v4.14: JSON path ops bind tighter than comparisons (5)
27218        // and additive (7) so `doc->'k' = 'v'` parses correctly.
27219        // Same rung as the multiplicative ops.
27220        Token::JsonGet => (BinOp::JsonGet, 8),
27221        Token::JsonGetText => (BinOp::JsonGetText, 8),
27222        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
27223        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
27224        Token::JsonContains => (BinOp::JsonContains, 8),
27225        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
27226        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
27227        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
27228        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
27229        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
27230        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
27231        // v7.12.2 — `@@` binds at the comparison rung (looser than
27232        // arithmetic, tighter than AND / OR). PG places `@@` at
27233        // the same precedence as `=` / `<`, so we follow.
27234        Token::TsMatch => (BinOp::TsMatch, 5),
27235        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
27236        // PG places these at the comparison rung (same level as `=`),
27237        // so we follow.
27238        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
27239        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
27240        Token::InetContains => (BinOp::InetContains, 5),
27241        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
27242        Token::InetOverlap => (BinOp::InetOverlap, 5),
27243        // v7.39 (round 508) — the geometric and pattern-order predicates
27244        // ride the comparison rung, as every other predicate does.
27245        Token::Intersects => (BinOp::Intersects, 5),
27246        Token::IsBelow => (BinOp::IsBelow, 5),
27247        Token::IsAbove => (BinOp::IsAbove, 5),
27248        Token::PatternLt => (BinOp::PatternLt, 5),
27249        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
27250        Token::PatternGt => (BinOp::PatternGt, 5),
27251        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
27252        // `@@@` is the old spelling of `@@` and means exactly it.
27253        Token::TsMatchOld => (BinOp::TsMatch, 5),
27254        _ => return None,
27255    };
27256    Some(pair)
27257}
27258
27259#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27260// `as f32` here is intentional: vector elements widen / narrow into f32 on
27261// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
27262// past ~15 decimal digits — both are acceptable for a fixed-precision
27263// pgvector column.
27264/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
27265/// implicit table alias and break trailing clauses. WITH lands
27266/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
27267/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
27268/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
27269/// / VALUES / FOR / LATERAL — all of which would otherwise be
27270/// silently swallowed by `parse_optional_alias`.
27271fn is_alias_stopword(s: &str) -> bool {
27272    matches!(
27273        s.to_ascii_lowercase().as_str(),
27274        "with"
27275            | "on"
27276            | "where"
27277            | "having"
27278            | "group"
27279            | "order"
27280            | "limit"
27281            | "offset"
27282            | "union"
27283            | "except"
27284            | "intersect"
27285            | "returning"
27286            | "set"
27287            | "values"
27288            | "for"
27289            | "window"
27290            | "tablesample"
27291            | "lateral"
27292            | "left"
27293            | "right"
27294            | "inner"
27295            | "outer"
27296            | "full"
27297            | "cross"
27298            | "join"
27299            | "natural"
27300            | "using"
27301            | "fetch"
27302    )
27303}
27304
27305fn extract_numeric_literal(e: &Expr) -> Option<f32> {
27306    match e {
27307        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
27308        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
27309        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
27310        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
27311        // so scale the divisor by hand instead of `f32::powi`.)
27312        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27313            let mut div = 1.0f32;
27314            for _ in 0..*scale {
27315                div *= 10.0;
27316            }
27317            Some(*unscaled as f32 / div)
27318        }
27319        Expr::Unary {
27320            op: UnOp::Neg,
27321            expr,
27322        } => extract_numeric_literal(expr).map(|x| -x),
27323        _ => None,
27324    }
27325}
27326
27327/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
27328/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
27329/// negative. Returns `None` if any pair fails to parse or no pair is found.
27330///
27331/// Recognised units (case-insensitive, optional trailing `s`):
27332/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
27333/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
27334/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
27335/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
27336/// (PG-canonical: DST and month-boundary semantics depend on this).
27337/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
27338/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
27339/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
27340#[allow(clippy::cast_possible_truncation)]
27341fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
27342    let mut months: i64 = 0;
27343    let mut days: i64 = 0;
27344    let mut micros: i64 = 0;
27345    let mut in_time = false;
27346    let mut num = alloc::string::String::new();
27347    for ch in rest.chars() {
27348        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
27349            num.push(ch);
27350            continue;
27351        }
27352        if ch == 'T' || ch == 't' {
27353            if !num.is_empty() {
27354                return None;
27355            }
27356            in_time = true;
27357            continue;
27358        }
27359        let n: f64 = num.parse().ok()?;
27360        num.clear();
27361        match (ch, in_time) {
27362            ('Y' | 'y', false) => months += (n * 12.0) as i64,
27363            ('M', false) => months += n as i64,
27364            ('W' | 'w', false) => days += (n * 7.0) as i64,
27365            ('D' | 'd', false) => days += n as i64,
27366            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
27367            ('M', true) => micros += (n * 60_000_000.0) as i64,
27368            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
27369            _ => return None,
27370        }
27371    }
27372    if !num.is_empty() {
27373        return None;
27374    }
27375    Some((
27376        i32::try_from(months).ok()?,
27377        i32::try_from(days).ok()?,
27378        micros,
27379    ))
27380}
27381
27382/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
27383/// leading `-` negates the whole value). Rejects date-like strings.
27384fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
27385    let (neg, body) = match s.strip_prefix('-') {
27386        Some(b) => (true, b),
27387        None => (false, s),
27388    };
27389    let (y, m) = body.split_once('-')?;
27390    let years: i32 = y.parse().ok()?;
27391    let mons: i32 = m.parse().ok()?;
27392    if years < 0 || mons < 0 {
27393        return None;
27394    }
27395    let total = years.checked_mul(12)?.checked_add(mons)?;
27396    Some((if neg { -total } else { total }, 0, 0))
27397}
27398
27399/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
27400/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
27401fn parse_interval_clock(tok: &str) -> Option<i64> {
27402    let (neg, body) = match tok.strip_prefix('-') {
27403        Some(r) => (true, r),
27404        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
27405    };
27406    let mut it = body.split(':');
27407    let h: i64 = it.next()?.parse().ok()?;
27408    let m: i64 = it.next()?.parse().ok()?;
27409    let s_tok = it.next().unwrap_or("0");
27410    if it.next().is_some() {
27411        return None;
27412    }
27413    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27414        let sec: i64 = sec.parse().ok()?;
27415        let mut f = alloc::string::String::from(frac);
27416        while f.len() < 6 {
27417            f.push('0');
27418        }
27419        f.truncate(6);
27420        let fus: i64 = f.parse().ok()?;
27421        sec.checked_mul(1_000_000)?.checked_add(fus)?
27422    } else {
27423        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27424    };
27425    let total = h
27426        .checked_mul(3_600_000_000)?
27427        .checked_add(m.checked_mul(60_000_000)?)?
27428        .checked_add(sec_us)?;
27429    Some(if neg { -total } else { total })
27430}
27431
27432/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27433/// every spelling PG accepts (measured against live PG18.4, not guessed):
27434/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27435/// Before this, the unit table matched long names only, with an ad-hoc
27436/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27437/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27438/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27439/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27440/// fractional) both read from this one table now.
27441fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27442    let u = raw.to_ascii_lowercase();
27443    Some(match u.as_str() {
27444        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27445            "microsecond"
27446        }
27447        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27448            "millisecond"
27449        }
27450        "second" | "seconds" | "sec" | "secs" | "s" => "second",
27451        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27452        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27453        "day" | "days" | "d" => "day",
27454        "week" | "weeks" | "w" => "week",
27455        "month" | "months" | "mon" | "mons" => "month",
27456        "year" | "years" | "yr" | "yrs" | "y" => "year",
27457        "decade" | "decades" | "dec" | "decs" => "decade",
27458        "century" | "centuries" | "cent" | "c" => "century",
27459        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27460        _ => return None,
27461    })
27462}
27463
27464/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27465/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27467pub(crate) enum IntervalField {
27468    Year,
27469    Month,
27470    Day,
27471    Hour,
27472    Minute,
27473    Second,
27474}
27475
27476/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27477/// spellings aren't standard for the qualifier position, so only the singular
27478/// forms are accepted.
27479/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27480/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27481/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27482/// take a `'1 2'` style literal — are not read here; they stay a parse
27483/// error rather than being silently misread.)
27484/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27485///
27486/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27487/// to do with a `@@` engine setting, and an unset one reads NULL rather
27488/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27489/// were the same node and `SELECT @x` answered "Unknown system variable".)
27490/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27491/// not see a session override — measured, after `SET autocommit=0`,
27492/// `@@global.autocommit` is still 1.
27493///
27494/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27495/// the parser's nesting budget is tuned against, and building these
27496/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27497/// wall `parse_left_right_atom` and friends were factored out for).
27498#[inline(never)]
27499fn variable_ref_atom(raw: &str) -> Expr {
27500    let user_var = !raw.starts_with("@@");
27501    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27502    Expr::FunctionCall {
27503        name: String::from(if user_var {
27504            "__spg_user_var"
27505        } else {
27506            "__spg_session_var"
27507        }),
27508        args: alloc::vec![Expr::Literal(Literal::String(bare))],
27509    }
27510}
27511
27512fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27513    let Token::Ident(s) = tok else { return None };
27514    Some(match () {
27515        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27516        () if s.eq_ignore_ascii_case("second") => "second",
27517        () if s.eq_ignore_ascii_case("minute") => "minute",
27518        () if s.eq_ignore_ascii_case("hour") => "hour",
27519        () if s.eq_ignore_ascii_case("day") => "day",
27520        () if s.eq_ignore_ascii_case("week") => "week",
27521        () if s.eq_ignore_ascii_case("month") => "month",
27522        () if s.eq_ignore_ascii_case("quarter") => "quarter",
27523        () if s.eq_ignore_ascii_case("year") => "year",
27524        () => return None,
27525    })
27526}
27527
27528/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27529/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27530/// which constructs the value at run time. Only the slot the unit names
27531/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27532/// slot the builtin has (months and fractional seconds respectively).
27533fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27534    let zero = || Expr::Literal(Literal::Integer(0));
27535    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27536        lhs: alloc::boxed::Box::new(qty.clone()),
27537        op,
27538        rhs: alloc::boxed::Box::new(by),
27539    };
27540    // (years, months, weeks, days, hours, mins, secs)
27541    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27542    match unit {
27543        "year" => args[0] = qty,
27544        "quarter" => {
27545            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27546        }
27547        "month" => args[1] = qty,
27548        "week" => args[2] = qty,
27549        "day" => args[3] = qty,
27550        "hour" => args[4] = qty,
27551        "minute" => args[5] = qty,
27552        "second" => args[6] = qty,
27553        // The builtin's seconds slot takes a fraction, so microseconds ride
27554        // it scaled down; the divisor is a NUMERIC literal so the division
27555        // stays exact rather than going through a float.
27556        "microsecond" => {
27557            args[6] = scaled(
27558                crate::ast::BinOp::Div,
27559                Expr::Literal(Literal::Numeric {
27560                    unscaled: 1_000_000,
27561                    scale: 0,
27562                }),
27563            );
27564        }
27565        _ => args[3] = qty,
27566    }
27567    Expr::FunctionCall {
27568        name: alloc::string::String::from("make_interval"),
27569        args,
27570    }
27571}
27572
27573/// `(count, unit)` → `(months, days, micros)`.
27574fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27575    let n: i64 = count.trim().parse().ok()?;
27576    Some(match unit {
27577        "microsecond" => (0, 0, n),
27578        "second" => (0, 0, n.checked_mul(1_000_000)?),
27579        "minute" => (0, 0, n.checked_mul(60_000_000)?),
27580        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27581        "day" => (0, i32::try_from(n).ok()?, 0),
27582        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27583        "month" => (i32::try_from(n).ok()?, 0, 0),
27584        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27585        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27586        _ => return None,
27587    })
27588}
27589
27590fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27591    let Token::Ident(s) = tok else { return None };
27592    Some(match () {
27593        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27594        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27595        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27596        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27597        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27598        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27599        () => return None,
27600    })
27601}
27602
27603/// v7.39 (read01 round 102) — interpret an interval literal under a field
27604/// qualifier. Returns `(months, days, micros)`.
27605///
27606/// * A single field applied to a bare number sets which unit the number means,
27607///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27608///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27609/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27610/// * Every other range, and any literal a single field can't read as a plain
27611///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27612///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27613///   like PG, and the qualifier there only bounds precision.
27614fn interpret_qualified_interval(
27615    text: &str,
27616    (f1, f2): (IntervalField, Option<IntervalField>),
27617) -> Option<(i32, i32, i64)> {
27618    if let Some(f2) = f2 {
27619        if f1 == IntervalField::Year && f2 == IntervalField::Month {
27620            if let Some(m) = parse_year_month_literal(text) {
27621                return Some((m, 0, 0));
27622            }
27623        }
27624        return parse_interval_text(text);
27625    }
27626    // Single field: reinterpret a bare number; otherwise the default parse.
27627    let trimmed = text.trim();
27628    if let Ok(val) = trimmed.parse::<f64>() {
27629        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27630        #[allow(clippy::cast_possible_truncation)]
27631        let whole = val as i64;
27632        #[allow(clippy::cast_possible_truncation)]
27633        let secs_micros = {
27634            let m = val * 1_000_000.0;
27635            if m >= 0.0 {
27636                (m + 0.5) as i64
27637            } else {
27638                (m - 0.5) as i64
27639            }
27640        };
27641        return Some(match f1 {
27642            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27643            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27644            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27645            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27646            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27647            IntervalField::Second => (0, 0, secs_micros),
27648        });
27649    }
27650    parse_interval_text(text)
27651}
27652
27653/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27654fn parse_year_month_literal(text: &str) -> Option<i32> {
27655    let t = text.trim();
27656    let (neg, body) = match t.strip_prefix('-') {
27657        Some(r) => (true, r),
27658        None => (false, t.strip_prefix('+').unwrap_or(t)),
27659    };
27660    let mut it = body.split('-');
27661    let years: i32 = it.next()?.trim().parse().ok()?;
27662    let months: i32 = match it.next() {
27663        Some(m) => m.trim().parse().ok()?,
27664        None => 0,
27665    };
27666    if it.next().is_some() {
27667        return None;
27668    }
27669    let total = years.checked_mul(12)?.checked_add(months)?;
27670    Some(if neg { -total } else { total })
27671}
27672
27673pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27674    // v7.38.19 — the two infinities, answered as the three extreme
27675    // fields PostgreSQL itself puts on the wire for them:
27676    //
27677    //   COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27678    //     … 7fffffffffffffff 7fffffff 7fffffff
27679    //
27680    // So no caller has to know the spelling — every one of them already
27681    // reads the three numbers, and `IntervalKind::from_fields` names
27682    // what they mean.
27683    //
27684    // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27685    // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27686    // infinity. Interval takes the full word, in any case.
27687    {
27688        let word = s.trim();
27689        let word = word.strip_prefix('@').map_or(word, str::trim);
27690        let (neg, body) = match word.strip_prefix('-') {
27691            Some(rest) => (true, rest.trim_start()),
27692            None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27693        };
27694        if body.eq_ignore_ascii_case("infinity") {
27695            return Some(if neg {
27696                (i32::MIN, i32::MIN, i64::MIN)
27697            } else {
27698                (i32::MAX, i32::MAX, i64::MAX)
27699            });
27700        }
27701    }
27702    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27703    // `@` is decorative; a trailing `ago` negates the whole interval.
27704    let mut trimmed = s.trim();
27705    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27706    let mut negate = false;
27707    if let Some(rest) = trimmed
27708        .strip_suffix("ago")
27709        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27710    {
27711        negate = true;
27712        trimmed = rest.trim();
27713    }
27714    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27715        let (mo, d, us) = v?;
27716        if negate {
27717            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27718        } else {
27719            Some((mo, d, us))
27720        }
27721    };
27722    let s = trimmed;
27723    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27724    // are single tokens, not the `<n> <unit>` pair form handled below.
27725    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27726        return finish(parse_iso8601_interval(rest));
27727    }
27728    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27729        if let Some(iv) = parse_year_month_interval(trimmed) {
27730            return finish(Some(iv));
27731        }
27732    }
27733    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27734    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27735    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27736    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27737        if let Ok(n) = trimmed.parse::<i64>() {
27738            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27739        }
27740        if let Ok(f) = trimmed.parse::<f64>() {
27741            if f.is_finite() {
27742                #[allow(clippy::cast_possible_truncation)]
27743                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27744            }
27745        }
27746    }
27747    // v7.39 (round 243) — PG accepts the number and unit run together
27748    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27749    // the `<n> <unit>` pair loop below sees them as two.
27750    let raw_parts: Vec<&str> = s.split_whitespace().collect();
27751    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27752    for p in raw_parts {
27753        let boundary = p
27754            .char_indices()
27755            .find(|(i, c)| {
27756                *i > 0
27757                    && c.is_ascii_alphabetic()
27758                    && p[..*i]
27759                        .chars()
27760                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27761                    && p[..*i].chars().any(|d| d.is_ascii_digit())
27762            })
27763            .map(|(i, _)| i);
27764        match boundary {
27765            Some(i) => {
27766                parts.push(&p[..i]);
27767                parts.push(&p[i..]);
27768            }
27769            None => parts.push(p),
27770        }
27771    }
27772    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27773    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27774    // remains is the `<n> <unit>` pair form handled below.
27775    let mut clock_us: i64 = 0;
27776    let mut had_clock = false;
27777    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27778        clock_us = parse_interval_clock(parts[pos])?;
27779        parts.remove(pos);
27780        had_clock = true;
27781    }
27782    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27783    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27784    let mut lone_days: i32 = 0;
27785    if had_clock && parts.len() == 1 {
27786        if let Ok(n) = parts[0].parse::<i64>() {
27787            lone_days = i32::try_from(n).ok()?;
27788            parts.clear();
27789        }
27790    }
27791    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27792        return None;
27793    }
27794    let mut months: i32 = 0;
27795    let mut days: i32 = lone_days;
27796    let mut micros: i64 = clock_us;
27797    let mut i = 0;
27798    while i < parts.len() {
27799        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27800        if let Ok(n) = parts[i].parse::<i64>() {
27801            match unit_stripped {
27802                "microsecond" => micros = micros.checked_add(n)?,
27803                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27804                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27805                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27806                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27807                "day" => {
27808                    let n32 = i32::try_from(n).ok()?;
27809                    days = days.checked_add(n32)?;
27810                }
27811                "week" => {
27812                    let n32 = i32::try_from(n).ok()?;
27813                    days = days.checked_add(n32.checked_mul(7)?)?;
27814                }
27815                "month" => {
27816                    let n32 = i32::try_from(n).ok()?;
27817                    months = months.checked_add(n32)?;
27818                }
27819                "year" => {
27820                    let n32 = i32::try_from(n).ok()?;
27821                    months = months.checked_add(n32.checked_mul(12)?)?;
27822                }
27823                // v7.39 (read01 timestamp.c) — the larger calendar units.
27824                "decade" => {
27825                    let n32 = i32::try_from(n).ok()?;
27826                    months = months.checked_add(n32.checked_mul(120)?)?;
27827                }
27828                "century" => {
27829                    let n32 = i32::try_from(n).ok()?;
27830                    months = months.checked_add(n32.checked_mul(1200)?)?;
27831                }
27832                "millennium" => {
27833                    let n32 = i32::try_from(n).ok()?;
27834                    months = months.checked_add(n32.checked_mul(12000)?)?;
27835                }
27836                _ => return None,
27837            }
27838        } else if let Ok(f) = parts[i].parse::<f64>() {
27839            // Fractional units cascade down to the next-finer field the way
27840            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27841            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27842            // no_std: f64 has no trunc/fract/round methods, so do them with
27843            // casts (toward-zero) + explicit round-half-away-from-zero.
27844            #[allow(clippy::cast_possible_truncation)]
27845            fn round_i64(x: f64) -> i64 {
27846                if x >= 0.0 {
27847                    (x + 0.5) as i64
27848                } else {
27849                    (x - 0.5) as i64
27850                }
27851            }
27852            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27853            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27854                const DAY_US: f64 = 86_400_000_000.0;
27855                let whole = d as i64; // truncates toward zero
27856                let frac = d - whole as f64;
27857                *days = days.checked_add(i32::try_from(whole).ok()?)?;
27858                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27859                Some(())
27860            }
27861            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27862            match unit_stripped {
27863                "microsecond" => micros = micros.checked_add(round_i64(f))?,
27864                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27865                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27866                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27867                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27868                "day" => add_days_frac(&mut days, &mut micros, f)?,
27869                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27870                "month" => {
27871                    let whole = f as i64;
27872                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27873                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27874                }
27875                "year" => {
27876                    let m = f * 12.0;
27877                    let whole = m as i64;
27878                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27879                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27880                }
27881                _ => return None,
27882            }
27883        } else {
27884            return None;
27885        }
27886        i += 2;
27887    }
27888    finish(Some((months, days, micros)))
27889}
27890
27891/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
27892/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
27893/// `interval` is intentionally absent (handled by its own parser arm).
27894/// Returns `None` for names that aren't sensible as a bare typed literal, so
27895/// the caller falls back to treating the ident as a column reference.
27896fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
27897    Some(match ident {
27898        "date" => CastTarget::Date,
27899        "timestamp" | "datetime" => CastTarget::Timestamp,
27900        "timestamptz" => CastTarget::Timestamptz,
27901        "bool" | "boolean" => CastTarget::Bool,
27902        "int" | "integer" | "int4" => CastTarget::Int,
27903        "bigint" | "int8" => CastTarget::BigInt,
27904        "float8" | "double precision" => CastTarget::Float,
27905        "uuid" => CastTarget::Uuid,
27906        "bytea" => CastTarget::Bytea,
27907        "json" => CastTarget::Json,
27908        "jsonb" => CastTarget::Jsonb,
27909        // Types without a dedicated CastTarget variant flow through the
27910        // generic Named path (engine resolves via column_type_to_data_type).
27911        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
27912        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
27913        | "money" | "bit" | "varbit"
27914        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
27915        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
27916        // Range / multirange types likewise.
27917        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
27918        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
27919        | "datemultirange" | "tsmultirange" | "tstzmultirange"
27920        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
27921        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
27922            CastTarget::Named(alloc::string::String::from(ident))
27923        }
27924        _ => return None,
27925    })
27926}
27927
27928/// v7.12.4 — map a bare type-name identifier (the form that
27929/// appears in a function arg list or RETURNS clause) to a
27930/// [`ColumnTypeName`]. Returns `None` for unknown / extension
27931/// types so the caller can preserve them as
27932/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
27933///
27934/// Subset of the full column-type grammar — we deliberately
27935/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
27936/// here because function-arg types in v7.12.4 are mostly the
27937/// bare form (`text`, `int`, `bytea`, …).
27938/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
27939/// than being `name TYPE`?
27940///
27941/// The multi-word spellings SQL allows for a bare argument type, each
27942/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
27943///
27944/// NOTE this list also exists in `spg-storage`, which computes the
27945/// signature key from the rendered argument text and has to reach the
27946/// same verdict. The two crates are siblings — neither depends on the
27947/// other — and each already carries its own table of type spellings
27948/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
27949/// there), so this follows the structure rather than inventing new
27950/// duplication. Recorded as V49.
27951pub fn is_multiword_type_phrase(phrase: &str) -> bool {
27952    let t = phrase.trim().to_ascii_lowercase();
27953    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
27954    matches!(
27955        base,
27956        "double precision"
27957            | "character varying"
27958            | "bit varying"
27959            | "timestamp with time zone"
27960            | "timestamp without time zone"
27961            | "time with time zone"
27962            | "time without time zone"
27963            | "national character"
27964            | "national character varying"
27965    )
27966}
27967
27968fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
27969    Some(match ident.to_ascii_lowercase().as_str() {
27970        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
27971        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
27972        "bigint" => ColumnTypeName::BigInt,
27973        "float" | "double" => ColumnTypeName::Float,
27974        // v7.39 (round 269) — real is 32-bit.
27975        "real" | "float4" => ColumnTypeName::Real,
27976        "text" => ColumnTypeName::Text,
27977        "bool" | "boolean" => ColumnTypeName::Bool,
27978        "date" => ColumnTypeName::Date,
27979        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
27980        "timestamptz" => ColumnTypeName::Timestamptz,
27981        "json" => ColumnTypeName::Json,
27982        "jsonb" => ColumnTypeName::Jsonb,
27983        "bytea" | "bytes" => ColumnTypeName::Bytes,
27984        "tsvector" => ColumnTypeName::TsVector,
27985        "tsquery" => ColumnTypeName::TsQuery,
27986        "uuid" => ColumnTypeName::Uuid,
27987        "interval" => ColumnTypeName::Interval,
27988        "time" => ColumnTypeName::Time,
27989        "year" => ColumnTypeName::Year,
27990        "timetz" => ColumnTypeName::TimeTz,
27991        "money" => ColumnTypeName::Money,
27992        _ => return None,
27993    })
27994}
27995
27996/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
27997/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
27998///
27999/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
28000/// / embedded SQL land in v7.12.5+):
28001///
28002/// ```text
28003///   body          := [ws] block [ws]
28004///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
28005///   stmt          := assign | return
28006///   assign        := assign_target := expr
28007///   assign_target := ( NEW | OLD ) . ident | ident
28008///   return        := RETURN ( NEW | OLD | NULL | expr )
28009/// ```
28010///
28011/// `expr` is parsed by recursing into the regular `Parser` — so a
28012/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
28013/// NEW.subject || ' ' || NEW.sender)` body shape works without
28014/// the body parser knowing what `to_tsvector` is.
28015///
28016/// Errors here cause the caller to fall back to
28017/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
28018/// successful, but the executor will refuse to invoke the
28019/// function with an "unparseable body" error.
28020/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
28021/// from the crate root as `spg_sql::parse_function_body`.
28022pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28023    parse_plpgsql_body(body)
28024}
28025
28026fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28027    // Use the regular lexer on the body text. The trailing
28028    // `END;` may or may not have a semicolon; the lexer treats
28029    // both forms identically.
28030    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
28031        message: alloc::format!("plpgsql body lex error: {e}"),
28032        token_pos: 0,
28033    })?;
28034    let mut parser = Parser::new(tokens);
28035    parser.parse_plpgsql_block()
28036}
28037
28038/// v7.39 (GUC) — the textual body of a SET value, for list joining.
28039fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
28040    match v {
28041        crate::ast::SetValue::String(s)
28042        | crate::ast::SetValue::Ident(s)
28043        | crate::ast::SetValue::Number(s) => s.clone(),
28044        crate::ast::SetValue::Default => "DEFAULT".into(),
28045    }
28046}
28047
28048/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
28049/// contains an aggregate call at ITS OWN query level (recursion stops at
28050/// sublink boundaries — a sublink's aggregates belong to the sublink).
28051/// Backs the "aggregate functions are not allowed in a recursive query's
28052/// recursive term" well-formedness check.
28053/// v7.40.0 — is this the name of an aggregate? The same list
28054/// `expr_has_toplevel_aggregate` walks, exposed so the grouping-set
28055/// rewrite can leave an aggregate's ARGUMENT alone.
28056pub(crate) fn is_aggregate_function_name(name: &str) -> bool {
28057    AGG_NAMES.iter().any(|a| name.eq_ignore_ascii_case(a))
28058}
28059
28060const AGG_NAMES: &[&str] = &[
28061    "count",
28062    "sum",
28063    "min",
28064    "max",
28065    "avg",
28066    "string_agg",
28067    "array_agg",
28068    "bool_and",
28069    "bool_or",
28070    "every",
28071    "any_value",
28072    "json_agg",
28073    "jsonb_agg",
28074    "json_object_agg",
28075    "jsonb_object_agg",
28076    "bit_and",
28077    "bit_or",
28078    "bit_xor",
28079    "var_pop",
28080    "var_samp",
28081    "variance",
28082    "std",
28083    "stddev",
28084    "stddev_pop",
28085    "stddev_samp",
28086    "range_agg",
28087    "range_intersect_agg",
28088    "percentile_cont",
28089    "percentile_disc",
28090    "mode",
28091    "corr",
28092    "covar_pop",
28093    "covar_samp",
28094];
28095
28096fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
28097    match e {
28098        Expr::AggregateOrdered { .. } => true,
28099        Expr::FunctionCall { name, args } => {
28100            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
28101                || args.iter().any(expr_has_toplevel_aggregate)
28102        }
28103        Expr::NamedArg { expr, .. }
28104        | Expr::Variadic(expr)
28105        | Expr::Unary { expr, .. }
28106        | Expr::Cast { expr, .. }
28107        | Expr::IsNull { expr, .. }
28108        | Expr::FieldAccess { base: expr, .. }
28109        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
28110        Expr::Binary { lhs, rhs, .. } => {
28111            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
28112        }
28113        Expr::Like { expr, pattern, .. } => {
28114            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
28115        }
28116        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
28117        Expr::InList { expr, list, .. } => {
28118            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
28119        }
28120        Expr::ArraySubscript { target, index } => {
28121            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
28122        }
28123        Expr::ArraySlice { target, lo, hi } => {
28124            expr_has_toplevel_aggregate(target)
28125                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
28126                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
28127        }
28128        Expr::AnyAll { expr, array, .. } => {
28129            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
28130        }
28131        Expr::Case {
28132            operand,
28133            branches,
28134            else_branch,
28135        } => {
28136            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
28137                || branches
28138                    .iter()
28139                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
28140                || else_branch
28141                    .as_deref()
28142                    .is_some_and(expr_has_toplevel_aggregate)
28143        }
28144        // The outer-level operands of a sublink can aggregate; the sublink's
28145        // own body cannot leak its aggregates up here.
28146        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
28147        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
28148            row.iter().any(expr_has_toplevel_aggregate)
28149        }
28150        _ => false,
28151    }
28152}
28153
28154/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
28155/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
28156/// named table anywhere in its subtree. A plain FROM derived table is NOT a
28157/// sublink and is legal in a recursive term, so it is not walked here.
28158fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
28159    let mut exprs: Vec<&Expr> = Vec::new();
28160    for it in &s.items {
28161        if let crate::ast::SelectItem::Expr { expr, .. } = it {
28162            exprs.push(expr);
28163        }
28164    }
28165    if let Some(w) = &s.where_ {
28166        exprs.push(w);
28167    }
28168    if let Some(h) = &s.having {
28169        exprs.push(h);
28170    }
28171    if let Some(g) = &s.group_by {
28172        exprs.extend(g.iter());
28173    }
28174    if let Some(from) = &s.from {
28175        for j in &from.joins {
28176            if let Some(on) = &j.on {
28177                exprs.push(on);
28178            }
28179        }
28180    }
28181    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
28182}
28183
28184/// Does this expression contain a sublink whose subquery mentions `name`?
28185fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
28186    match e {
28187        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
28188        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
28189        Expr::InSubquery { expr, subquery, .. } => {
28190            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
28191        }
28192        Expr::RowInSubquery { row, subquery, .. } => {
28193            row.iter().any(|x| expr_sublink_mentions(x, name))
28194                || select_mentions_table(subquery, name)
28195        }
28196        Expr::RowCmpSubquery { row, subquery, .. } => {
28197            row.iter().any(|x| expr_sublink_mentions(x, name))
28198                || select_mentions_table(subquery, name)
28199        }
28200        Expr::NamedArg { expr, .. }
28201        | Expr::Variadic(expr)
28202        | Expr::Unary { expr, .. }
28203        | Expr::Cast { expr, .. }
28204        | Expr::IsNull { expr, .. }
28205        | Expr::FieldAccess { base: expr, .. }
28206        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
28207        Expr::Binary { lhs, rhs, .. } => {
28208            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
28209        }
28210        Expr::Like { expr, pattern, .. } => {
28211            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
28212        }
28213        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
28214            args.iter().any(|x| expr_sublink_mentions(x, name))
28215        }
28216        Expr::InList { expr, list, .. } => {
28217            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
28218        }
28219        Expr::ArraySubscript { target, index } => {
28220            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
28221        }
28222        Expr::ArraySlice { target, lo, hi } => {
28223            expr_sublink_mentions(target, name)
28224                || lo
28225                    .as_deref()
28226                    .is_some_and(|x| expr_sublink_mentions(x, name))
28227                || hi
28228                    .as_deref()
28229                    .is_some_and(|x| expr_sublink_mentions(x, name))
28230        }
28231        Expr::AnyAll { expr, array, .. } => {
28232            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
28233        }
28234        Expr::Case {
28235            operand,
28236            branches,
28237            else_branch,
28238        } => {
28239            operand
28240                .as_deref()
28241                .is_some_and(|x| expr_sublink_mentions(x, name))
28242                || branches
28243                    .iter()
28244                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
28245                || else_branch
28246                    .as_deref()
28247                    .is_some_and(|x| expr_sublink_mentions(x, name))
28248        }
28249        _ => false,
28250    }
28251}
28252
28253/// Does this SELECT (in full — FROM tables, derived tables, its own
28254/// sublinks, and union arms) mention the named table?
28255fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
28256    if let Some(from) = &s.from {
28257        if from.primary.name.eq_ignore_ascii_case(name) {
28258            return true;
28259        }
28260        if let Some(sub) = &from.primary.lateral_subquery
28261            && select_mentions_table(sub, name)
28262        {
28263            return true;
28264        }
28265        for j in &from.joins {
28266            if j.table.name.eq_ignore_ascii_case(name) {
28267                return true;
28268            }
28269            if let Some(sub) = &j.table.lateral_subquery
28270                && select_mentions_table(sub, name)
28271            {
28272                return true;
28273            }
28274        }
28275    }
28276    if select_has_self_ref_in_sublink(s, name) {
28277        return true;
28278    }
28279    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
28280}
28281
28282/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
28283/// row count, the way PG evaluates one before applying it.
28284///
28285/// `None` = not a constant (a column, a subquery, a function call).
28286/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
28287/// message stands in for LIMIT / OFFSET, which the caller substitutes.
28288/// All wordings were read off live PG 18.4.
28289fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
28290    use crate::ast::{BinOp, Expr, Literal, UnOp};
28291    match e {
28292        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
28293        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
28294            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
28295        }
28296        // PG coerces a string by its CONTENT, and fails on the value.
28297        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
28298            |_| {
28299                Err(alloc::format!(
28300                    "invalid input syntax for type bigint: \"{t}\""
28301                ))
28302            },
28303            |n| Ok(i128::from(n)),
28304        )),
28305        Expr::Literal(Literal::Bool(_)) => Some(Err(
28306            "argument of {L} must be type bigint, not type boolean".into(),
28307        )),
28308        Expr::Unary {
28309            op: UnOp::Neg,
28310            expr,
28311        } => match fold_limit_constant(expr)? {
28312            Ok(v) => Some(Ok(-v)),
28313            e @ Err(_) => Some(e),
28314        },
28315        Expr::Binary { lhs, op, rhs } => {
28316            let a = match fold_limit_constant(lhs)? {
28317                Ok(v) => v,
28318                e @ Err(_) => return Some(e),
28319            };
28320            let b = match fold_limit_constant(rhs)? {
28321                Ok(v) => v,
28322                e @ Err(_) => return Some(e),
28323            };
28324            let out = match op {
28325                BinOp::Add => a.checked_add(b),
28326                BinOp::Sub => a.checked_sub(b),
28327                BinOp::Mul => a.checked_mul(b),
28328                BinOp::Div if b != 0 => a.checked_div(b),
28329                BinOp::Div => return Some(Err("division by zero".into())),
28330                BinOp::Mod if b != 0 => a.checked_rem(b),
28331                BinOp::Mod => return Some(Err("division by zero".into())),
28332                _ => return None,
28333            };
28334            // PG evaluates the arithmetic in the operand's own type, so an
28335            // int-by-int product that leaves int range fails there — before
28336            // the row count is ever looked at.
28337            match out {
28338                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
28339                    Some(Err("integer out of range".into()))
28340                }
28341                Some(v) => Some(Ok(v)),
28342                None => Some(Err("integer out of range".into())),
28343            }
28344        }
28345        _ => None,
28346    }
28347}
28348
28349/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
28350/// cast, which is what makes `LIMIT 2.5` keep three rows.
28351fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
28352    if scale == 0 {
28353        return unscaled;
28354    }
28355    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
28356        return 0;
28357    };
28358    let neg = unscaled < 0;
28359    let mag = unscaled.unsigned_abs() as i128;
28360    let rounded = (mag + div / 2) / div;
28361    if neg { -rounded } else { rounded }
28362}
28363
28364#[cfg(test)]
28365mod tests {
28366    use super::*;
28367    use alloc::string::ToString;
28368
28369    fn parse(s: &str) -> Statement {
28370        parse_statement(s).expect("parse ok")
28371    }
28372
28373    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
28374    // `tables`, `partition`, etc. are unreserved keywords per PG's
28375    // `pg_get_keywords()` and MUST be usable as column / table /
28376    // alias names. Pre-T4 every drop-in user whose schema had one
28377    // of these as a column name (sentori events.release, mailrs
28378    // messages.index in some forks) blew the parser up at CREATE
28379    // TABLE time with "expected identifier, got Release". The
28380    // generalisation lives in `unreserved_keyword_text` + the
28381    // `expect_ident_like` and `parse_atom` arms that consult it.
28382    #[test]
28383    fn release_usable_as_column_name_in_create_table() {
28384        let stmt =
28385            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
28386        if let Statement::CreateTable(t) = stmt {
28387            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
28388            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
28389        } else {
28390            panic!("expected CreateTable");
28391        }
28392    }
28393
28394    #[test]
28395    fn release_usable_as_column_ref_in_select_projection() {
28396        // The sentori `0003_partition_events.sql` INSERT-SELECT
28397        // walk references `release` in both column lists; the
28398        // projection-side use exercises `parse_atom`'s relaxed
28399        // identifier set.
28400        parse("SELECT id, release, payload FROM events WHERE id = 1");
28401    }
28402
28403    #[test]
28404    fn release_usable_as_column_ref_in_insert_column_list() {
28405        // INSERT INTO t (id, release, payload) VALUES (…)
28406        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
28407    }
28408
28409    #[test]
28410    fn alter_column_drop_not_null_uses_keyword_drop_token() {
28411        // Sentori `0013_audit_tombstone.sql` issues
28412        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
28413        // emits Token::Drop (not Ident("drop")); the parser must
28414        // accept both in the ALTER COLUMN sub-dispatch.
28415        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
28416    }
28417
28418    #[test]
28419    fn create_index_accepts_parenthesised_expression_key() {
28420        // sentori `0040_events_bundle_idx.sql` shape — JSONB
28421        // expression index. Pre-T4 the parser bailed at the
28422        // inner `(` with "expected column ident or expression,
28423        // got LParen". The Token::LParen arm in CREATE INDEX
28424        // routes through the expression parser instead.
28425        parse(
28426            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28427             ON events ((payload->'bundle'->>'id'))",
28428        );
28429    }
28430
28431    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28432    // surface as parse errors, never stack overflows (embed hosts
28433    // abort on overflow).
28434    /// The nesting budget is a COUNT; what it has to fit inside is a
28435    /// number of BYTES, and only one of those two is stable across
28436    /// compiler versions. Round 847 measured 30,336 bytes per level
28437    /// after a toolchain move, which puts 64 levels at 1.94 MB and
28438    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28439    /// aborted instead of erroring, which is precisely the outcome it
28440    /// exists to rule out.
28441    ///
28442    /// So the budget is metered rather than assumed. The ceiling leaves
28443    /// the depth SPG advertises fitting in a default 2 MiB thread with
28444    /// room to spare, in the debug build, where frames are widest.
28445    #[test]
28446    fn nesting_frame_cost_stays_under_ceiling() {
28447        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28448        // thread keeps a margin for whatever called the parser.
28449        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28450
28451        frame_meter::reset();
28452        let depth = frame_meter::SAMPLE_HI + 8;
28453        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28454        parse(&sql);
28455
28456        let per_level = frame_meter::bytes_per_level();
28457        {
28458            extern crate std;
28459            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28460        }
28461        assert!(
28462            per_level <= CEILING,
28463            "{per_level} bytes per nesting level exceeds {CEILING}; \
28464             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28465             in parse_expr_inner / parse_unary rather than lowering the \
28466             depth or widening the stack.",
28467            per_level * MAX_NEST_DEPTH
28468        );
28469    }
28470
28471    #[test]
28472    fn nesting_budget_errors_cleanly() {
28473        let depth = MAX_NEST_DEPTH + 50;
28474        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28475        let err = parse_statement(&sql).expect_err("must reject");
28476        assert!(err.message.contains("nests deeper"), "{err:?}");
28477        // Within budget still parses.
28478        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28479        parse(&sql);
28480    }
28481
28482    #[test]
28483    fn binary_chain_budget_errors_cleanly() {
28484        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28485        let err = parse_statement(&sql).expect_err("must reject");
28486        assert!(err.message.contains("chained binary"), "{err:?}");
28487        // Within budget still parses (chain depth ≤ budget is safe
28488        // for recursive eval/drop on 2 MiB stacks).
28489        let sql = format!("SELECT 1{}", " + 1".repeat(200));
28490        parse(&sql);
28491    }
28492
28493    #[test]
28494    fn in_list_unaffected_by_chain_budget() {
28495        // Flat InList: 20k elements parse fine and stay flat.
28496        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28497        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28498        let Statement::Select(s) = parse(&sql) else {
28499            panic!("expected select")
28500        };
28501        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28502            panic!("expected flat InList, got {:?}", s.where_)
28503        };
28504        assert_eq!(list.len(), 20_000);
28505        assert!(!negated);
28506    }
28507
28508    fn lit_int(n: i64) -> Expr {
28509        Expr::Literal(Literal::Integer(n))
28510    }
28511
28512    fn col(name: &str) -> Expr {
28513        Expr::Column(ColumnName {
28514            qualifier: None,
28515            name: name.into(),
28516        })
28517    }
28518
28519    #[test]
28520    fn select_single_integer() {
28521        let s = parse("SELECT 1");
28522        let Statement::Select(s) = s else {
28523            panic!("expected SELECT")
28524        };
28525        assert_eq!(s.items.len(), 1);
28526        assert!(s.from.is_none());
28527        assert!(s.where_.is_none());
28528    }
28529
28530    #[test]
28531    fn select_multiple_literal_kinds() {
28532        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28533        let Statement::Select(s) = s else {
28534            panic!("expected SELECT")
28535        };
28536        assert_eq!(s.items.len(), 5);
28537    }
28538
28539    #[test]
28540    fn select_wildcard_from_table() {
28541        let s = parse("SELECT * FROM users");
28542        let Statement::Select(s) = s else {
28543            panic!("expected SELECT")
28544        };
28545        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28546        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28547    }
28548
28549    #[test]
28550    fn select_with_table_alias() {
28551        let s = parse("SELECT * FROM users AS u");
28552        let Statement::Select(s) = s else {
28553            panic!("expected SELECT")
28554        };
28555        let t = &s.from.as_ref().unwrap().primary;
28556        assert_eq!(t.name, "users");
28557        assert_eq!(t.alias.as_deref(), Some("u"));
28558    }
28559
28560    #[test]
28561    fn select_with_where_eq() {
28562        let s = parse("SELECT a FROM t WHERE a = 1");
28563        let Statement::Select(s) = s else {
28564            panic!("expected SELECT")
28565        };
28566        let w = s.where_.unwrap();
28567        assert_eq!(
28568            w,
28569            Expr::Binary {
28570                lhs: Box::new(col("a")),
28571                op: BinOp::Eq,
28572                rhs: Box::new(lit_int(1)),
28573            }
28574        );
28575    }
28576
28577    #[test]
28578    fn arithmetic_precedence() {
28579        let s = parse("SELECT 1 + 2 * 3");
28580        let Statement::Select(s) = s else {
28581            panic!("expected SELECT")
28582        };
28583        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28584            panic!("wildcard?")
28585        };
28586        assert_eq!(
28587            expr,
28588            &Expr::Binary {
28589                lhs: Box::new(lit_int(1)),
28590                op: BinOp::Add,
28591                rhs: Box::new(Expr::Binary {
28592                    lhs: Box::new(lit_int(2)),
28593                    op: BinOp::Mul,
28594                    rhs: Box::new(lit_int(3)),
28595                }),
28596            }
28597        );
28598    }
28599
28600    #[test]
28601    fn parentheses_override_precedence() {
28602        let s = parse("SELECT (1 + 2) * 3");
28603        let Statement::Select(s) = s else {
28604            panic!("expected SELECT")
28605        };
28606        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28607            panic!()
28608        };
28609        assert_eq!(
28610            expr,
28611            &Expr::Binary {
28612                lhs: Box::new(Expr::Binary {
28613                    lhs: Box::new(lit_int(1)),
28614                    op: BinOp::Add,
28615                    rhs: Box::new(lit_int(2)),
28616                }),
28617                op: BinOp::Mul,
28618                rhs: Box::new(lit_int(3)),
28619            }
28620        );
28621    }
28622
28623    #[test]
28624    fn not_binds_below_comparison() {
28625        // `NOT a = 1` should parse as `NOT (a = 1)`.
28626        let s = parse("SELECT NOT a = 1 FROM t");
28627        let Statement::Select(s) = s else {
28628            panic!("expected SELECT")
28629        };
28630        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28631            panic!()
28632        };
28633        assert_eq!(
28634            expr,
28635            &Expr::Unary {
28636                op: UnOp::Not,
28637                expr: Box::new(Expr::Binary {
28638                    lhs: Box::new(col("a")),
28639                    op: BinOp::Eq,
28640                    rhs: Box::new(lit_int(1)),
28641                }),
28642            }
28643        );
28644    }
28645
28646    #[test]
28647    fn unary_minus_binds_above_multiplication() {
28648        // `-a * 2` should be `(-a) * 2`.
28649        let s = parse("SELECT -a * 2 FROM t");
28650        let Statement::Select(s) = s else {
28651            panic!("expected SELECT")
28652        };
28653        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28654            panic!()
28655        };
28656        assert_eq!(
28657            expr,
28658            &Expr::Binary {
28659                lhs: Box::new(Expr::Unary {
28660                    op: UnOp::Neg,
28661                    expr: Box::new(col("a")),
28662                }),
28663                op: BinOp::Mul,
28664                rhs: Box::new(lit_int(2)),
28665            }
28666        );
28667    }
28668
28669    #[test]
28670    fn qualified_column() {
28671        let s = parse("SELECT t.col FROM t");
28672        let Statement::Select(s) = s else {
28673            panic!("expected SELECT")
28674        };
28675        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28676            panic!()
28677        };
28678        assert_eq!(
28679            expr,
28680            &Expr::Column(ColumnName {
28681                qualifier: Some("t".into()),
28682                name: "col".into()
28683            })
28684        );
28685    }
28686
28687    #[test]
28688    fn select_item_alias_with_as() {
28689        let s = parse("SELECT a AS y FROM t");
28690        let Statement::Select(s) = s else {
28691            panic!("expected SELECT")
28692        };
28693        let SelectItem::Expr { alias, .. } = &s.items[0] else {
28694            panic!()
28695        };
28696        assert_eq!(alias.as_deref(), Some("y"));
28697    }
28698
28699    #[test]
28700    fn trailing_semicolon_accepted() {
28701        let s = parse("SELECT 1;");
28702        let Statement::Select(s) = s else {
28703            panic!("expected SELECT")
28704        };
28705        assert_eq!(s.items.len(), 1);
28706    }
28707
28708    #[test]
28709    fn boolean_chain_with_and_or_not() {
28710        // (NOT a) OR (b AND (NOT c))
28711        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28712        let Statement::Select(s) = s else {
28713            panic!("expected SELECT")
28714        };
28715        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28716            panic!()
28717        };
28718        let expected = Expr::Binary {
28719            lhs: Box::new(Expr::Unary {
28720                op: UnOp::Not,
28721                expr: Box::new(col("a")),
28722            }),
28723            op: BinOp::Or,
28724            rhs: Box::new(Expr::Binary {
28725                lhs: Box::new(col("b")),
28726                op: BinOp::And,
28727                rhs: Box::new(Expr::Unary {
28728                    op: UnOp::Not,
28729                    expr: Box::new(col("c")),
28730                }),
28731            }),
28732        };
28733        assert_eq!(expr, &expected);
28734    }
28735
28736    #[test]
28737    fn empty_input_errors() {
28738        // v7.14.0 — pg_dump preambles emit several comment-only
28739        // / blank-line statements that collapse to Statement::
28740        // Empty rather than a parse error. The old "SELECT in
28741        // message" assertion is stale; verify the new contract:
28742        // empty / whitespace / comment-only input parses to
28743        // Statement::Empty.
28744        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28745        assert!(matches!(
28746            parse_statement("  \n\t ").unwrap(),
28747            Statement::Empty
28748        ));
28749        // Sanity: malformed-but-non-empty still errors.
28750        assert!(parse_statement("SELECT FROM WHERE").is_err());
28751    }
28752
28753    #[test]
28754    fn unmatched_paren_errors() {
28755        assert!(parse_statement("SELECT (1 + 2").is_err());
28756    }
28757
28758    #[test]
28759    fn display_round_trip_simple_select() {
28760        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28761        let text = original.to_string();
28762        let again = parse_statement(&text).expect("re-parse");
28763        assert_eq!(original, again);
28764    }
28765
28766    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28767
28768    #[test]
28769    fn create_table_single_column() {
28770        let s = parse("CREATE TABLE foo (a INT)");
28771        let Statement::CreateTable(c) = s else {
28772            panic!("expected CreateTable")
28773        };
28774        assert_eq!(c.name, "foo");
28775        assert_eq!(c.columns.len(), 1);
28776        assert_eq!(c.columns[0].name, "a");
28777        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28778        assert!(c.columns[0].nullable);
28779    }
28780
28781    #[test]
28782    fn create_table_multi_column_with_not_null_mix() {
28783        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28784        let Statement::CreateTable(c) = s else {
28785            panic!()
28786        };
28787        assert_eq!(c.columns.len(), 4);
28788        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28789        assert!(!c.columns[0].nullable);
28790        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28791        assert!(c.columns[1].nullable);
28792        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28793        assert!(!c.columns[2].nullable);
28794        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28795    }
28796
28797    #[test]
28798    fn create_table_bigint_supported() {
28799        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28800        let Statement::CreateTable(c) = s else {
28801            panic!()
28802        };
28803        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28804    }
28805
28806    #[test]
28807    fn create_table_vector_default_is_f32() {
28808        let s = parse("CREATE TABLE t (v VECTOR(128))");
28809        let Statement::CreateTable(c) = s else {
28810            panic!()
28811        };
28812        assert_eq!(
28813            c.columns[0].ty,
28814            ColumnTypeName::Vector {
28815                dim: 128,
28816                encoding: VecEncoding::F32,
28817            },
28818        );
28819    }
28820
28821    #[test]
28822    fn create_table_vector_using_sq8() {
28823        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28824        // Case-insensitive on both `USING` and the encoding name.
28825        for sql in [
28826            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28827            "CREATE TABLE t (v VECTOR(128) using sq8)",
28828        ] {
28829            let s = parse(sql);
28830            let Statement::CreateTable(c) = s else {
28831                panic!()
28832            };
28833            assert_eq!(
28834                c.columns[0].ty,
28835                ColumnTypeName::Vector {
28836                    dim: 128,
28837                    encoding: VecEncoding::Sq8,
28838                },
28839                "{sql}",
28840            );
28841        }
28842    }
28843
28844    #[test]
28845    fn create_table_vector_using_unknown_errors() {
28846        // v7.16.1 — the inline `USING <encoding>` shape on
28847        // CREATE TABLE column defs was withdrawn before
28848        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28849        // (col vector_<metric>_ops)`; the parser now rejects
28850        // USING at column-list position with a clearer
28851        // "expected ',' or ')'" message. Test asserts the
28852        // current rejection, not the old "unknown vector
28853        // encoding" string.
28854        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28855        assert!(
28856            err.message.contains("USING")
28857                || err.message.contains("using")
28858                || err.message.contains("')'")
28859                || err.message.contains("','"),
28860            "expected USING/column-list rejection, got: {}",
28861            err.message
28862        );
28863    }
28864
28865    #[test]
28866    fn vector_using_sq8_display_roundtrips() {
28867        // The Display impl must produce text that re-parses to the
28868        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28869        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28870        let Statement::CreateTable(c) = s else {
28871            panic!()
28872        };
28873        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28874    }
28875
28876    #[test]
28877    fn parser_recognises_placeholders() {
28878        use crate::ast::{Expr, SelectItem, Statement};
28879        // $N in expression position parses as Expr::Placeholder(N).
28880        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28881        let Statement::Select(sel) = s else { panic!() };
28882        assert!(matches!(
28883            sel.items[0],
28884            SelectItem::Expr {
28885                expr: Expr::Placeholder(1),
28886                alias: None
28887            }
28888        ));
28889        // $2 + 1
28890        let SelectItem::Expr {
28891            expr: Expr::Binary { lhs, rhs, .. },
28892            ..
28893        } = &sel.items[1]
28894        else {
28895            panic!()
28896        };
28897        assert!(matches!(**lhs, Expr::Placeholder(2)));
28898        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
28899        // WHERE x = $3
28900        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
28901            panic!()
28902        };
28903        assert!(matches!(**rhs, Expr::Placeholder(3)));
28904    }
28905
28906    #[test]
28907    fn parser_rejects_dollar_zero() {
28908        // $0 is not valid in PG; the lexer rejects it.
28909        assert!(parse_statement("SELECT $0").is_err());
28910    }
28911
28912    #[test]
28913    fn placeholder_display_roundtrips() {
28914        // The Display impl must produce text that re-lexes to the
28915        // same Placeholder token.
28916        let s = parse("SELECT $42 FROM t");
28917        let printed = s.to_string();
28918        assert!(printed.contains("$42"));
28919        let again = parse(&printed);
28920        assert_eq!(s, again);
28921    }
28922
28923    #[test]
28924    fn alter_index_rebuild_bare() {
28925        use crate::ast::{AlterIndexTarget, Statement};
28926        let s = parse("ALTER INDEX my_idx REBUILD");
28927        let Statement::AlterIndex(a) = s else {
28928            panic!("expected AlterIndex, got {s:?}")
28929        };
28930        assert_eq!(a.name, "my_idx");
28931        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
28932    }
28933
28934    #[test]
28935    fn alter_index_rebuild_with_encoding() {
28936        use crate::ast::{AlterIndexTarget, Statement};
28937        for (sql, want) in [
28938            (
28939                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
28940                VecEncoding::F32,
28941            ),
28942            (
28943                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
28944                VecEncoding::Sq8,
28945            ),
28946            (
28947                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28948                VecEncoding::F16,
28949            ),
28950        ] {
28951            let s = parse(sql);
28952            let Statement::AlterIndex(a) = s else {
28953                panic!("{sql}: expected AlterIndex")
28954            };
28955            assert_eq!(a.name, "my_idx");
28956            assert_eq!(
28957                a.target,
28958                AlterIndexTarget::Rebuild {
28959                    encoding: Some(want)
28960                },
28961                "{sql}"
28962            );
28963        }
28964    }
28965
28966    #[test]
28967    fn alter_index_rebuild_unknown_encoding_errors() {
28968        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
28969        assert!(
28970            err.message.contains("unknown vector encoding"),
28971            "got: {}",
28972            err.message
28973        );
28974    }
28975
28976    #[test]
28977    fn alter_index_rebuild_display_roundtrips() {
28978        for (input, want) in [
28979            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
28980            (
28981                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28982                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
28983            ),
28984            (
28985                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28986                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
28987            ),
28988        ] {
28989            let s = parse(input);
28990            assert_eq!(s.to_string(), want);
28991        }
28992    }
28993
28994    #[test]
28995    fn create_table_unknown_type_defers_to_engine() {
28996        // v4.9 picked XML as a parse-time "unsupported column
28997        // type" probe. v7.17.0 Phase 1.4 changed the contract:
28998        // an unknown type ident parses as Text + `user_type_ref`
28999        // so CREATE TABLE can resolve user-defined enum / domain
29000        // types — rejection of truly-unknown types moved to the
29001        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
29002        // to a first-class built-in, so this probe switched to a
29003        // synthetic name nothing in the lexer will ever recognise.
29004        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
29005        let Statement::CreateTable(t) = stmt else {
29006            panic!("expected CreateTable");
29007        };
29008        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
29009    }
29010
29011    #[test]
29012    fn create_table_missing_table_keyword_errors() {
29013        assert!(parse_statement("CREATE x (a INT)").is_err());
29014    }
29015
29016    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
29017    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
29018
29019    #[test]
29020    fn parse_create_table_partition_by_range() {
29021        use crate::ast::{PartitionBySpec, PartitionKindAst};
29022        let stmt = parse_statement(
29023            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
29024             payload JSONB) PARTITION BY RANGE (ts)",
29025        )
29026        .unwrap();
29027        let Statement::CreateTable(t) = stmt else {
29028            panic!("expected CreateTable");
29029        };
29030        assert!(t.partition_of.is_none(), "parent has no partition_of");
29031        assert_eq!(t.columns.len(), 3);
29032        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
29033        assert_eq!(
29034            by,
29035            &PartitionBySpec {
29036                kind: PartitionKindAst::Range,
29037                key_columns: alloc::vec!["ts".to_string()],
29038            }
29039        );
29040        // Display round-trip preserves the suffix. `quote_ident`
29041        // only adds double quotes when the ident needs escaping, so
29042        // a plain `ts` survives bare here.
29043        assert!(
29044            t.to_string().contains("PARTITION BY RANGE (ts)"),
29045            "Display lost PARTITION BY suffix: {t}"
29046        );
29047    }
29048
29049    #[test]
29050    fn parse_create_table_partition_of_range() {
29051        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
29052        let stmt = parse_statement(
29053            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
29054             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
29055        )
29056        .unwrap();
29057        let Statement::CreateTable(t) = stmt else {
29058            panic!("expected CreateTable");
29059        };
29060        assert!(t.columns.is_empty(), "child inherits columns from parent");
29061        assert!(t.partition_by.is_none());
29062        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29063        assert_eq!(of.parent_name, "events_partitioned");
29064        let PartitionOfSpec { bounds, .. } = of.clone();
29065        match bounds {
29066            PartitionOfBoundsAst::Range { lower, upper } => {
29067                assert!(lower.to_string().contains("2026-06-01"));
29068                assert!(upper.to_string().contains("2026-07-01"));
29069            }
29070            other => panic!("expected Range, got {other:?}"),
29071        }
29072        // Display round-trip emits the FOR VALUES tail. `quote_ident`
29073        // skips quotes when not required, so the parent name appears
29074        // bare here.
29075        let s = t.to_string();
29076        assert!(
29077            s.contains("PARTITION OF events_partitioned"),
29078            "Display lost PARTITION OF: {s}"
29079        );
29080        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
29081        assert!(s.contains(") TO ("), "Display lost TO: {s}");
29082    }
29083
29084    #[test]
29085    fn parse_create_table_partition_of_default() {
29086        use crate::ast::PartitionOfBoundsAst;
29087        let stmt =
29088            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
29089                .unwrap();
29090        let Statement::CreateTable(t) = stmt else {
29091            panic!("expected CreateTable");
29092        };
29093        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29094        assert_eq!(of.parent_name, "events_partitioned");
29095        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
29096        assert!(
29097            t.to_string()
29098                .contains("PARTITION OF events_partitioned DEFAULT"),
29099            "Display lost DEFAULT: {t}"
29100        );
29101    }
29102
29103    #[test]
29104    fn parse_create_table_partition_by_list() {
29105        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
29106        // child with `FOR VALUES IN (lit, lit, …)`.
29107        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29108        let parent =
29109            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
29110                .unwrap();
29111        let Statement::CreateTable(t) = parent else {
29112            panic!("expected CreateTable");
29113        };
29114        let Some(PartitionBySpec {
29115            kind,
29116            ref key_columns,
29117        }) = t.partition_by
29118        else {
29119            panic!("expected PARTITION BY");
29120        };
29121        assert_eq!(kind, PartitionKindAst::List);
29122        assert_eq!(*key_columns, vec!["region".to_string()]);
29123        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
29124
29125        let child = parse_statement(
29126            "CREATE TABLE events_apac PARTITION OF events_listed \
29127             FOR VALUES IN ('jp', 'kr', 'tw')",
29128        )
29129        .unwrap();
29130        let Statement::CreateTable(c) = child else {
29131            panic!("expected CreateTable");
29132        };
29133        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29134        let PartitionOfBoundsAst::List { values } = &of.bounds else {
29135            panic!("expected List bounds, got {:?}", of.bounds);
29136        };
29137        assert_eq!(values.len(), 3);
29138        let disp = c.to_string();
29139        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
29140    }
29141
29142    #[test]
29143    fn parse_create_table_partition_by_hash() {
29144        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
29145        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
29146        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29147        let parent =
29148            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
29149        let Statement::CreateTable(t) = parent else {
29150            panic!("expected CreateTable");
29151        };
29152        let Some(PartitionBySpec {
29153            kind,
29154            ref key_columns,
29155        }) = t.partition_by
29156        else {
29157            panic!("expected PARTITION BY");
29158        };
29159        assert_eq!(kind, PartitionKindAst::Hash);
29160        assert_eq!(*key_columns, vec!["id".to_string()]);
29161        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
29162
29163        let child = parse_statement(
29164            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
29165             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
29166        )
29167        .unwrap();
29168        let Statement::CreateTable(c) = child else {
29169            panic!("expected CreateTable");
29170        };
29171        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29172        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
29173            panic!("expected Hash bounds");
29174        };
29175        assert_eq!(modulus, 4);
29176        assert_eq!(remainder, 0);
29177        let disp = c.to_string();
29178        assert!(
29179            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
29180            "Display lost HASH bounds: {disp}"
29181        );
29182
29183        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
29184        let bad = parse_statement(
29185            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
29186             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
29187        );
29188        let msg = format!("{}", bad.unwrap_err());
29189        assert!(
29190            msg.contains("REMAINDER") && msg.contains("MODULUS"),
29191            "expected REMAINDER/MODULUS validation error: {msg}"
29192        );
29193    }
29194
29195    #[test]
29196    fn parse_create_table_partition_of_rejects_columns() {
29197        // v7.37.6-B contract: PARTITION OF children inherit columns
29198        // from the parent; an explicit list MUST surface as a parse
29199        // error rather than getting silently ignored.
29200        let err = parse_statement(
29201            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
29202             FOR VALUES FROM ('a') TO ('b')",
29203        );
29204        assert!(err.is_err(), "expected parse error for explicit columns");
29205        let msg = format!("{}", err.unwrap_err());
29206        assert!(
29207            msg.contains("PARTITION OF") && msg.contains("column"),
29208            "error should mention PARTITION OF + columns: {msg}"
29209        );
29210    }
29211
29212    #[test]
29213    fn insert_single_value() {
29214        let s = parse("INSERT INTO foo VALUES (42)");
29215        let Statement::Insert(i) = s else {
29216            panic!("expected Insert")
29217        };
29218        assert_eq!(i.table, "foo");
29219        assert_eq!(i.rows.len(), 1);
29220        assert_eq!(i.rows[0].len(), 1);
29221        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
29222    }
29223
29224    #[test]
29225    fn insert_multi_value_with_mixed_literals() {
29226        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
29227        let Statement::Insert(i) = s else { panic!() };
29228        assert_eq!(i.rows.len(), 1);
29229        assert_eq!(i.rows[0].len(), 5);
29230    }
29231
29232    #[test]
29233    fn insert_missing_into_errors() {
29234        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
29235    }
29236
29237    #[test]
29238    fn create_table_round_trip() {
29239        let original =
29240            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
29241        let text = original.to_string();
29242        let again = parse_statement(&text).expect("re-parse");
29243        assert_eq!(original, again);
29244    }
29245
29246    #[test]
29247    fn insert_round_trip_with_negation_and_string() {
29248        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
29249        let text = original.to_string();
29250        let again = parse_statement(&text).expect("re-parse");
29251        assert_eq!(original, again);
29252    }
29253
29254    #[test]
29255    fn unknown_keyword_at_statement_start_errors() {
29256        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
29257        // the top-level dispatch still has no branch to take.
29258        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
29259        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
29260    }
29261
29262    // --- v0.8 CREATE INDEX --------------------------------------------------
29263
29264    #[test]
29265    fn create_index_basic() {
29266        let s = parse("CREATE INDEX idx_id ON users (id)");
29267        let Statement::CreateIndex(c) = s else {
29268            panic!("expected CreateIndex")
29269        };
29270        assert_eq!(c.name, "idx_id");
29271        assert_eq!(c.table, "users");
29272        assert_eq!(c.column, "id");
29273    }
29274
29275    #[test]
29276    fn create_index_missing_on_errors() {
29277        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
29278    }
29279
29280    #[test]
29281    fn create_index_missing_paren_errors() {
29282        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
29283    }
29284
29285    #[test]
29286    fn create_index_round_trip() {
29287        let original = parse("CREATE INDEX by_name ON users (name)");
29288        let again = parse_statement(&original.to_string()).unwrap();
29289        assert_eq!(original, again);
29290    }
29291
29292    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
29293
29294    #[test]
29295    fn create_unique_index_basic() {
29296        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
29297        let Statement::CreateIndex(c) = s else {
29298            panic!("expected CreateIndex");
29299        };
29300        assert!(c.is_unique);
29301        assert_eq!(c.column, "a");
29302        assert!(c.partial_predicate.is_none());
29303    }
29304
29305    #[test]
29306    fn create_unique_index_partial() {
29307        // mailrs's email_templates "one default per user" shape.
29308        let s = parse(
29309            "CREATE UNIQUE INDEX idx_email_templates_user_default \
29310             ON email_templates (user_address) WHERE is_default = true",
29311        );
29312        let Statement::CreateIndex(c) = s else {
29313            panic!("expected CreateIndex");
29314        };
29315        assert!(c.is_unique);
29316        assert_eq!(c.table, "email_templates");
29317        assert_eq!(c.column, "user_address");
29318        assert!(c.partial_predicate.is_some());
29319    }
29320
29321    #[test]
29322    fn create_unique_index_composite_with_predicate() {
29323        // mailrs's calendar_events instance: composite columns.
29324        let s = parse(
29325            "CREATE UNIQUE INDEX uq_calendar_events_instance \
29326             ON calendar_events (calendar_id, uid, recurrence_id) \
29327             WHERE recurrence_id IS NOT NULL",
29328        );
29329        let Statement::CreateIndex(c) = s else {
29330            panic!("expected CreateIndex");
29331        };
29332        assert!(c.is_unique);
29333        assert_eq!(c.column, "calendar_id");
29334        assert_eq!(
29335            c.extra_columns,
29336            vec!["uid".to_string(), "recurrence_id".to_string()]
29337        );
29338        assert!(c.partial_predicate.is_some());
29339    }
29340
29341    #[test]
29342    fn create_unique_index_using_btree_ok() {
29343        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
29344        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
29345    }
29346
29347    #[test]
29348    fn create_unique_index_using_hnsw_rejected() {
29349        let err =
29350            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
29351        assert!(err.message.contains("UNIQUE"), "{}", err.message);
29352    }
29353
29354    #[test]
29355    fn create_unique_index_round_trip() {
29356        let original = parse(
29357            "CREATE UNIQUE INDEX uq_calendar_events_master \
29358             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
29359        );
29360        let again = parse_statement(&original.to_string()).unwrap();
29361        assert_eq!(original, again);
29362    }
29363
29364    #[test]
29365    fn create_unique_without_index_errors() {
29366        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
29367        // v7.39 (round 340, V56) — PG 18.4, verbatim.
29368        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
29369    }
29370
29371    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
29372
29373    #[test]
29374    fn create_table_bytea_column() {
29375        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
29376        let Statement::CreateTable(c) = s else {
29377            panic!("expected CreateTable");
29378        };
29379        assert_eq!(c.columns.len(), 2);
29380        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
29381        assert!(!c.columns[1].nullable);
29382    }
29383
29384    #[test]
29385    fn create_table_bytes_alias_column() {
29386        let s = parse("CREATE TABLE t (blob BYTES)");
29387        let Statement::CreateTable(c) = s else {
29388            panic!("expected CreateTable");
29389        };
29390        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
29391    }
29392
29393    #[test]
29394    fn bytea_round_trip_display() {
29395        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
29396        let again = parse_statement(&original.to_string()).unwrap();
29397        assert_eq!(original, again);
29398    }
29399
29400    // --- v0.9 transactions -------------------------------------------------
29401
29402    #[test]
29403    fn begin_commit_rollback_parse_as_unit_variants() {
29404        let plain = crate::ast::TransactionModes::default();
29405        assert_eq!(parse("BEGIN"), Statement::Begin(plain));
29406        assert_eq!(parse("COMMIT"), Statement::Commit);
29407        // r1066 — PG synonyms pgbench's tpcb script relies on.
29408        assert_eq!(parse("END"), Statement::Commit);
29409        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
29410        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
29411        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
29412        // Trailing semicolons accepted too.
29413        assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
29414        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
29415        // statement (with or without the WORK/TRANSACTION noise word).
29416        assert_eq!(
29417            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
29418            Statement::Begin(crate::ast::TransactionModes {
29419                isolation: Some(IsolationLevel::RepeatableRead),
29420                read_only: None,
29421            })
29422        );
29423        assert_eq!(
29424            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29425            Statement::Begin(crate::ast::TransactionModes {
29426                isolation: Some(IsolationLevel::Serializable),
29427                read_only: None,
29428            })
29429        );
29430        // v7.39 — this line used to read
29431        //
29432        //     // A non-isolation mode keeps the session default (None).
29433        //     assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29434        //
29435        // which pinned the defect rather than catching it: the READ ONLY
29436        // was thrown away, so the statement opened an ordinary read-write
29437        // transaction and every write inside it was accepted. The
29438        // isolation level is still absent here, because this statement
29439        // does not name one — that part was right.
29440        assert_eq!(
29441            parse("BEGIN READ ONLY"),
29442            Statement::Begin(crate::ast::TransactionModes {
29443                isolation: None,
29444                read_only: Some(true),
29445            })
29446        );
29447        assert_eq!(
29448            parse("START TRANSACTION READ WRITE"),
29449            Statement::Begin(crate::ast::TransactionModes {
29450                isolation: None,
29451                read_only: Some(false),
29452            })
29453        );
29454        assert_eq!(
29455            parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29456            Statement::Begin(crate::ast::TransactionModes {
29457                isolation: Some(IsolationLevel::Serializable),
29458                read_only: Some(true),
29459            })
29460        );
29461    }
29462
29463    // --- v1.2: pgvector distance ops + ::vector cast --------------------
29464
29465    #[test]
29466    fn inner_product_binop_parses() {
29467        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29468        let Statement::Select(s) = s else { panic!() };
29469        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29470            panic!()
29471        };
29472        assert!(matches!(
29473            expr,
29474            Expr::Binary {
29475                op: BinOp::InnerProduct,
29476                ..
29477            }
29478        ));
29479    }
29480
29481    #[test]
29482    fn cosine_distance_binop_parses() {
29483        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29484        let Statement::Select(s) = s else { panic!() };
29485        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29486            panic!()
29487        };
29488        assert!(matches!(
29489            expr,
29490            Expr::Binary {
29491                op: BinOp::CosineDistance,
29492                ..
29493            }
29494        ));
29495    }
29496
29497    #[test]
29498    fn vector_cast_postfix_wraps_string_literal() {
29499        let s = parse("SELECT '[1,2,3]'::vector FROM t");
29500        let Statement::Select(s) = s else { panic!() };
29501        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29502            panic!()
29503        };
29504        assert!(matches!(
29505            expr,
29506            Expr::Cast {
29507                target: CastTarget::Vector,
29508                ..
29509            }
29510        ));
29511    }
29512
29513    #[test]
29514    fn unsupported_cast_target_errors() {
29515        // v7.37.5 ship triage promoted the parser to accept every
29516        // ident as a `CastTarget::Named(canonical)`; the engine
29517        // surfaces the "unsupported cast target" error at eval
29518        // time when `type_name_to_data_type` can't resolve it.
29519        // Parser-side error now requires a NON-ident after `::`
29520        // (e.g. a punctuation token).
29521        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29522        assert_eq!(err.message, "syntax error at or near \",\"");
29523    }
29524
29525    #[test]
29526    fn tx_statements_round_trip() {
29527        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29528            let original = parse(q);
29529            let again = parse_statement(&original.to_string()).unwrap();
29530            assert_eq!(original, again);
29531        }
29532    }
29533
29534    #[test]
29535    fn interval_text_parsing_units() {
29536        // v7.37.5 β — three-field shape `(months, days, micros)` so
29537        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29538        // Single unit.
29539        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29540        assert_eq!(
29541            parse_interval_text("24 hours"),
29542            Some((0, 0, 86_400_000_000))
29543        );
29544        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29545        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29546        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29547        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29548        // Compound spans accumulate per-dimension.
29549        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29550        assert_eq!(
29551            parse_interval_text("1 day 2 hours"),
29552            Some((0, 1, 7_200_000_000))
29553        );
29554        // Negative numbers carry through per-dimension.
29555        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29556        // Bad shapes return None.
29557        assert_eq!(parse_interval_text(""), None);
29558        assert_eq!(parse_interval_text("garbage"), None);
29559        assert_eq!(parse_interval_text("1 fortnight"), None);
29560        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29561        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29562        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29563        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29564        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29565    }
29566
29567    #[test]
29568    fn interval_literal_roundtrips_via_display() {
29569        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29570        let s = parsed.to_string();
29571        // Display preserves the original text verbatim.
29572        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29573        // And re-parsing yields a structurally equal statement.
29574        let again = parse_statement(&s).unwrap();
29575        assert_eq!(parsed, again);
29576    }
29577
29578    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29579
29580    #[test]
29581    fn parser_recognises_create_publication_bare() {
29582        let s = parse("CREATE PUBLICATION pub_a");
29583        let Statement::CreatePublication(p) = s else {
29584            panic!("expected CreatePublication, got {s:?}")
29585        };
29586        assert_eq!(p.name, "pub_a");
29587        assert_eq!(p.scope, PublicationScope::AllTables);
29588    }
29589
29590    #[test]
29591    fn parser_recognises_create_publication_for_all_tables() {
29592        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29593        let Statement::CreatePublication(p) = s else {
29594            panic!("expected CreatePublication, got {s:?}")
29595        };
29596        assert_eq!(p.name, "pub_a");
29597        assert_eq!(p.scope, PublicationScope::AllTables);
29598    }
29599
29600    #[test]
29601    fn parser_recognises_drop_publication() {
29602        let s = parse("DROP PUBLICATION pub_a");
29603        let Statement::DropPublication { name, .. } = s else {
29604            panic!("expected DropPublication, got {s:?}")
29605        };
29606        assert_eq!(name, "pub_a");
29607    }
29608
29609    #[test]
29610    fn parser_recognises_for_table_list() {
29611        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29612        let Statement::CreatePublication(p) = s else {
29613            panic!("expected CreatePublication, got {s:?}")
29614        };
29615        assert_eq!(p.name, "pub_a");
29616        let PublicationScope::ForTables(ts) = p.scope else {
29617            panic!("expected ForTables scope")
29618        };
29619        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29620    }
29621
29622    #[test]
29623    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29624        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29625        // is rejected (`invalid publication object list`; the old
29626        // test pinned an unverifiable "PG 19 accepts both" claim);
29627        // TABLES pairs with IN SCHEMA.
29628        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29629            .expect_err("bare FOR TABLES must reject");
29630        assert!(
29631            alloc::format!("{err}").contains("invalid publication object list"),
29632            "got: {err}"
29633        );
29634        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29635        let Statement::CreatePublication(p) = s else {
29636            panic!("expected CreatePublication, got {s:?}")
29637        };
29638        let PublicationScope::TablesInSchema(schema) = p.scope else {
29639            panic!("expected TablesInSchema")
29640        };
29641        assert_eq!(schema, "public");
29642    }
29643
29644    #[test]
29645    fn parser_recognises_for_all_tables_except_list() {
29646        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29647        let Statement::CreatePublication(p) = s else {
29648            panic!()
29649        };
29650        let PublicationScope::AllTablesExcept(ts) = p.scope else {
29651            panic!("expected AllTablesExcept")
29652        };
29653        assert_eq!(ts, alloc::vec!["t1", "t2"]);
29654    }
29655
29656    #[test]
29657    fn parser_rejects_for_table_with_empty_list() {
29658        // `FOR TABLE` with nothing after is a parse error.
29659        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29660            .expect_err("must error on empty list");
29661        // No specific message asserted — the call falls through to
29662        // expect_ident_like which yields "expected identifier, got …".
29663        assert!(!err.message.is_empty());
29664    }
29665
29666    #[test]
29667    fn parser_recognises_show_publications() {
29668        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29669        // bare ident in this position, NOT a reserved keyword.
29670        let s = parse("SHOW PUBLICATIONS");
29671        assert!(matches!(s, Statement::ShowPublications));
29672    }
29673
29674    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29675
29676    #[test]
29677    fn parser_recognises_create_subscription_single_publication() {
29678        let s = parse(
29679            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29680        );
29681        let Statement::CreateSubscription(c) = s else {
29682            panic!("expected CreateSubscription, got {s:?}")
29683        };
29684        assert_eq!(c.name, "sub_a");
29685        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29686        assert_eq!(c.publications, alloc::vec!["pub_a"]);
29687    }
29688
29689    #[test]
29690    fn parser_recognises_create_subscription_multi_publication() {
29691        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29692        let Statement::CreateSubscription(c) = s else {
29693            panic!()
29694        };
29695        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29696    }
29697
29698    #[test]
29699    fn parser_rejects_create_subscription_missing_connection() {
29700        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29701            .expect_err("must error on missing CONNECTION");
29702        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29703    }
29704
29705    #[test]
29706    fn parser_rejects_create_subscription_missing_publication() {
29707        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29708            .expect_err("must error on missing PUBLICATION");
29709        assert_eq!(err.message, "syntax error at end of input");
29710    }
29711
29712    #[test]
29713    fn parser_recognises_drop_subscription() {
29714        let s = parse("DROP SUBSCRIPTION sub_a");
29715        let Statement::DropSubscription { name, .. } = s else {
29716            panic!("expected DropSubscription, got {s:?}")
29717        };
29718        assert_eq!(name, "sub_a");
29719    }
29720
29721    #[test]
29722    fn parser_recognises_show_subscriptions() {
29723        let s = parse("SHOW SUBSCRIPTIONS");
29724        assert!(matches!(s, Statement::ShowSubscriptions));
29725    }
29726
29727    #[test]
29728    fn parser_recognises_wait_for_wal_position_no_timeout() {
29729        let s = parse("WAIT FOR WAL POSITION 12345");
29730        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29731            panic!("expected WaitForWalPosition, got {s:?}")
29732        };
29733        assert_eq!(pos, 12345);
29734        assert!(timeout_ms.is_none());
29735    }
29736
29737    #[test]
29738    fn parser_recognises_wait_for_wal_position_with_timeout() {
29739        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29740        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29741            panic!()
29742        };
29743        assert_eq!(pos, 67890);
29744        assert_eq!(timeout_ms, Some(5000));
29745    }
29746
29747    #[test]
29748    fn parser_rejects_wait_with_negative_position() {
29749        // The lexer treats `-` as a token; `expect_u64_literal`
29750        // only sees the Integer that follows, so the negative
29751        // arrives as a unary-minus expression at higher levels.
29752        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29753        // parse error one way or another.
29754        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29755        assert!(!err.message.is_empty());
29756    }
29757
29758    #[test]
29759    fn parser_recognises_bare_analyze() {
29760        let s = parse("ANALYZE");
29761        assert!(matches!(s, Statement::Analyze(None)));
29762    }
29763
29764    #[test]
29765    fn parser_recognises_analyze_with_table() {
29766        let s = parse("ANALYZE users");
29767        let Statement::Analyze(Some(name)) = s else {
29768            panic!("expected Analyze, got {s:?}")
29769        };
29770        assert_eq!(name, "users");
29771    }
29772
29773    #[test]
29774    fn parser_recognises_analyze_with_quoted_table() {
29775        let s = parse("ANALYZE \"Mixed Case\"");
29776        let Statement::Analyze(Some(name)) = s else {
29777            panic!()
29778        };
29779        assert_eq!(name, "Mixed Case");
29780    }
29781
29782    #[test]
29783    fn parser_rejects_analyze_with_garbage_token() {
29784        let err = parse_statement("ANALYZE 42").expect_err("must error");
29785        assert!(!err.message.is_empty());
29786    }
29787
29788    #[test]
29789    fn analyze_display_roundtrips() {
29790        for sql in ["ANALYZE", "ANALYZE users"] {
29791            let s = parse(sql);
29792            let printed = s.to_string();
29793            let again = parse_statement(&printed)
29794                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29795            assert_eq!(s, again);
29796        }
29797    }
29798
29799    #[test]
29800    fn wait_for_display_roundtrips() {
29801        for sql in [
29802            "WAIT FOR WAL POSITION 12345",
29803            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
29804        ] {
29805            let s = parse(sql);
29806            let printed = s.to_string();
29807            let again = parse_statement(&printed)
29808                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29809            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29810        }
29811    }
29812
29813    #[test]
29814    fn subscription_ddl_display_roundtrips() {
29815        for sql in [
29816            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
29817            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
29818            "DROP SUBSCRIPTION sub_a",
29819            "SHOW SUBSCRIPTIONS",
29820        ] {
29821            let s = parse(sql);
29822            let printed = s.to_string();
29823            let again = parse_statement(&printed)
29824                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29825            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29826        }
29827    }
29828
29829    #[test]
29830    fn parser_drop_dispatches_user_vs_publication() {
29831        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
29832        // tokenises DROP. Both targets must still parse.
29833        let s = parse("DROP USER 'alice'");
29834        let Statement::DropUser { name, .. } = s else {
29835            panic!("expected DropUser, got {s:?}")
29836        };
29837        assert_eq!(name, "alice");
29838        // And DROP PUBLICATION lands the new variant.
29839        let s = parse("DROP PUBLICATION p1");
29840        assert!(matches!(s, Statement::DropPublication { .. }));
29841    }
29842
29843    #[test]
29844    fn publication_ddl_display_roundtrips() {
29845        // Every CREATE PUBLICATION variant must Display → parse →
29846        // same AST. v6.1.3 covers all three scope shapes.
29847        for sql in [
29848            "CREATE PUBLICATION pub_a",
29849            "CREATE PUBLICATION pub_a FOR ALL TABLES",
29850            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29851            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29852            "DROP PUBLICATION pub_a",
29853            "SHOW PUBLICATIONS",
29854        ] {
29855            let s = parse(sql);
29856            let printed = s.to_string();
29857            let again = parse_statement(&printed)
29858                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29859            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29860        }
29861    }
29862
29863    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29864
29865    #[test]
29866    fn create_function_returns_trigger_plpgsql_minimal() {
29867        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29868        let s = parse(sql);
29869        let Statement::CreateFunction(f) = s else {
29870            panic!("expected CreateFunction");
29871        };
29872        assert_eq!(f.name, "noop");
29873        assert!(!f.or_replace);
29874        assert!(f.args.is_empty());
29875        assert!(matches!(f.returns, FunctionReturn::Trigger));
29876        assert_eq!(f.language, "plpgsql");
29877        let FunctionBody::PlPgSql(block) = f.body else {
29878            panic!("expected PlPgSql body");
29879        };
29880        assert_eq!(block.statements.len(), 1);
29881        assert!(matches!(
29882            block.statements[0],
29883            PlPgSqlStmt::Return(ReturnTarget::New)
29884        ));
29885    }
29886
29887    #[test]
29888    fn create_function_or_replace_with_assignment() {
29889        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
29890        // RETURN NEW.
29891        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
29892BEGIN
29893  NEW.search_vector := to_tsvector('english', NEW.subject);
29894  RETURN NEW;
29895END;
29896$$";
29897        let s = parse(sql);
29898        let Statement::CreateFunction(f) = s else {
29899            panic!("expected CreateFunction");
29900        };
29901        assert!(f.or_replace);
29902        let FunctionBody::PlPgSql(block) = &f.body else {
29903            panic!("expected PlPgSql body");
29904        };
29905        assert_eq!(block.statements.len(), 2);
29906        // First statement: NEW.search_vector := to_tsvector(...)
29907        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
29908            panic!("expected Assign as first stmt");
29909        };
29910        match target {
29911            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
29912            other => panic!("expected NEW.col, got {other:?}"),
29913        }
29914        // Second statement: RETURN NEW
29915        assert!(matches!(
29916            block.statements[1],
29917            PlPgSqlStmt::Return(ReturnTarget::New)
29918        ));
29919    }
29920
29921    #[test]
29922    fn create_trigger_after_insert_or_update() {
29923        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
29924        let s = parse(sql);
29925        let Statement::CreateTrigger(t) = s else {
29926            panic!("expected CreateTrigger");
29927        };
29928        assert_eq!(t.name, "tg");
29929        assert_eq!(t.table, "messages");
29930        assert_eq!(t.timing, TriggerTiming::After);
29931        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
29932        assert_eq!(t.for_each, TriggerForEach::Row);
29933        assert_eq!(t.function, "update_sv");
29934    }
29935
29936    #[test]
29937    fn create_trigger_before_delete_execute_procedure_alias() {
29938        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
29939        let sql =
29940            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
29941        let s = parse(sql);
29942        let Statement::CreateTrigger(t) = s else {
29943            panic!("expected CreateTrigger");
29944        };
29945        assert_eq!(t.timing, TriggerTiming::Before);
29946        assert_eq!(t.events, vec![TriggerEvent::Delete]);
29947    }
29948
29949    #[test]
29950    fn drop_trigger_if_exists_round_trips() {
29951        // No parser support for DROP TRIGGER yet — added in v7.12.5
29952        // alongside the broader DROP …{IF EXISTS} cleanup. The
29953        // AST + Display impls are in place so we round-trip via
29954        // construction:
29955        let s = Statement::DropTrigger {
29956            name: "tg".into(),
29957            table: "messages".into(),
29958            if_exists: true,
29959        };
29960        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
29961    }
29962
29963    #[test]
29964    fn trigger_ddl_display_roundtrips_through_parser() {
29965        // CREATE TRIGGER + its referenced CREATE FUNCTION must
29966        // Display → parse → same AST (modulo PL/pgSQL body
29967        // formatting which is parser-canonicalised).
29968        for sql in [
29969            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
29970            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
29971        ] {
29972            let s = parse(sql);
29973            let printed = s.to_string();
29974            let again = parse_statement(&printed)
29975                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29976            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29977        }
29978    }
29979}