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                // v7.40.11 — and it may HEAD a set-operation chain:
2260                // `VALUES (1) UNION ALL SELECT 2`. The CTE-body form has
2261                // done this since the recursive-seed work; the top-level
2262                // statement went straight to the tail and reported
2263                // `syntax error at or near "UNION"`.
2264                self.parse_setop_chain_into(&mut head)?;
2265                self.parse_select_tail_into(&mut head)?;
2266                Ok(Statement::Select(head))
2267            }
2268            // SQL-standard `TABLE name` shorthand for
2269            // `SELECT * FROM name` — pg_dump never emits it, but
2270            // psql users and PG docs use it constantly. Set-op
2271            // chains and the ORDER BY/LIMIT tail compose like any
2272            // SELECT head.
2273            Token::Table
2274                if matches!(
2275                    self.tokens.get(self.pos + 1),
2276                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2277                ) =>
2278            {
2279                let mut head = self.parse_table_shorthand()?;
2280                self.parse_setop_chain_into(&mut head)?;
2281                self.parse_select_tail_into(&mut head)?;
2282                Ok(Statement::Select(head))
2283            }
2284            // v7.9.27 — `DO $$ … $$ [LANGUAGE plpgsql]`. The
2285            // body is a dollar-quoted plpgsql block (lexer already
2286            // collapsed `$$…$$` into a single Token::String).
2287            // v7.16.2 — mailrs round-10 A.2: parse the body as a
2288            // real PlPgSqlBlock so the engine can EXECUTE it at
2289            // top level instead of silently swallowing. Pre-
2290            // v7.16.2 the parser threw the body away and the
2291            // engine returned CommandOk for the entire DO; that
2292            // turned `DO BEGIN … IF EXISTS ... THEN ALTER …; END
2293            // $$` into a SEV-1 silent no-op (the IF + the rename
2294            // were both invisible — mailrs's migrate-042 didn't
2295            // actually run). Now the body parses + executes;
2296            // EmbeddedSql inside the block runs immediately
2297            // against the engine (not deferred — we're at top
2298            // level, not inside a trigger row-write loop).
2299            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
2300                self.advance();
2301                let body_text = match self.advance() {
2302                    Token::String(s) => s,
2303                    other => {
2304                        return Err(self.err(alloc::format!(
2305                            "expected dollar-quoted body after DO, got {other:?}"
2306                        )));
2307                    }
2308                };
2309                // Optional `LANGUAGE <name>` trailer (idents only).
2310                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("language")) {
2311                    self.advance();
2312                    let _ = self.expect_ident_like()?;
2313                }
2314                // Parse the body — same shape CREATE FUNCTION
2315                // uses for trigger function bodies. If the body
2316                // doesn't parse cleanly we surface the error
2317                // (better than silent no-op).
2318                let block = parse_plpgsql_body(&body_text)?;
2319                Ok(Statement::DoBlock(block))
2320            }
2321            // v4.11: `WITH name AS (SELECT ...) [, ...] SELECT ...`.
2322            // WITH isn't a reserved token in our lexer — comes through
2323            // as `Token::Ident("with")` (case-insensitive).
2324            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with") => {
2325                self.advance();
2326                self.parse_with_cte_then_select()
2327            }
2328            // v4.26: `EXPLAIN [ANALYZE] <select>`. Comes through as
2329            // an identifier — not a reserved keyword.
2330            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("explain") => {
2331                self.advance();
2332                let mut analyze = false;
2333                let mut suggest = false;
2334                let mut costs_off = false;
2335                let mut buffers = false;
2336                let mut timing_off = false;
2337                let mut settings = false;
2338                let mut wal = false;
2339                let mut summary_off = false;
2340                let mut format = crate::ast::ExplainFormat::Text;
2341                // v6.8.3 + v7.37.7 — `EXPLAIN (option [, option…])`
2342                // syntax accepts SUGGEST + COSTS ON|OFF. Multiple
2343                // options are comma-separated. Booleans default to ON
2344                // when the value token is omitted (matches PG).
2345                if matches!(self.peek(), Token::LParen) {
2346                    self.advance();
2347                    loop {
2348                        let opt = match self.peek().clone() {
2349                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2350                            other => {
2351                                return Err(self.err(format!(
2352                                    "expected option keyword inside EXPLAIN (…), got {other:?}"
2353                                )));
2354                            }
2355                        };
2356                        self.advance();
2357                        if opt.eq_ignore_ascii_case("suggest") {
2358                            suggest = true;
2359                            // SUGGEST takes no explicit value today.
2360                        } else if opt.eq_ignore_ascii_case("costs") {
2361                            // PG syntax: `COSTS [ON | OFF]`. Default
2362                            // when value omitted is ON, so plain
2363                            // `COSTS` is a no-op. `COSTS OFF` flips.
2364                            // `ON` lexes to `Token::On` (reserved
2365                            // keyword in JOIN ... ON contexts); accept
2366                            // it alongside the bare Ident form so the
2367                            // grammar matches PG verbatim.
2368                            let value = match self.peek().clone() {
2369                                Token::On => {
2370                                    self.advance();
2371                                    true
2372                                }
2373                                Token::Ident(v) | Token::QuotedIdent(v)
2374                                    if v.eq_ignore_ascii_case("off") =>
2375                                {
2376                                    self.advance();
2377                                    false
2378                                }
2379                                Token::Ident(v) | Token::QuotedIdent(v)
2380                                    if v.eq_ignore_ascii_case("true") =>
2381                                {
2382                                    self.advance();
2383                                    true
2384                                }
2385                                _ => true,
2386                            };
2387                            costs_off = !value;
2388                        } else if opt.eq_ignore_ascii_case("analyze")
2389                            || opt.eq_ignore_ascii_case("analyse")
2390                        {
2391                            // v7.37.22 — `EXPLAIN (ANALYZE [ON|OFF]) <S>`.
2392                            // Same default-ON rule as ANALYZE keyword form.
2393                            let value = match self.peek().clone() {
2394                                Token::On => {
2395                                    self.advance();
2396                                    true
2397                                }
2398                                Token::Ident(v) | Token::QuotedIdent(v)
2399                                    if v.eq_ignore_ascii_case("off") =>
2400                                {
2401                                    self.advance();
2402                                    false
2403                                }
2404                                Token::Ident(v) | Token::QuotedIdent(v)
2405                                    if v.eq_ignore_ascii_case("true") =>
2406                                {
2407                                    self.advance();
2408                                    true
2409                                }
2410                                _ => true,
2411                            };
2412                            analyze = value;
2413                        } else if opt.eq_ignore_ascii_case("buffers") {
2414                            // v7.37.22 — `BUFFERS [ON|OFF]`.
2415                            let value = match self.peek().clone() {
2416                                Token::On => {
2417                                    self.advance();
2418                                    true
2419                                }
2420                                Token::Ident(v) | Token::QuotedIdent(v)
2421                                    if v.eq_ignore_ascii_case("off") =>
2422                                {
2423                                    self.advance();
2424                                    false
2425                                }
2426                                Token::Ident(v) | Token::QuotedIdent(v)
2427                                    if v.eq_ignore_ascii_case("true") =>
2428                                {
2429                                    self.advance();
2430                                    true
2431                                }
2432                                _ => true,
2433                            };
2434                            buffers = value;
2435                        } else if opt.eq_ignore_ascii_case("timing") {
2436                            // v7.37.22 — `TIMING [ON|OFF]`. OFF strips
2437                            // the measured wall-clock annotation.
2438                            let value = match self.peek().clone() {
2439                                Token::On => {
2440                                    self.advance();
2441                                    true
2442                                }
2443                                Token::Ident(v) | Token::QuotedIdent(v)
2444                                    if v.eq_ignore_ascii_case("off") =>
2445                                {
2446                                    self.advance();
2447                                    false
2448                                }
2449                                Token::Ident(v) | Token::QuotedIdent(v)
2450                                    if v.eq_ignore_ascii_case("true") =>
2451                                {
2452                                    self.advance();
2453                                    true
2454                                }
2455                                _ => true,
2456                            };
2457                            timing_off = !value;
2458                        } else if opt.eq_ignore_ascii_case("settings") {
2459                            settings = true;
2460                        } else if opt.eq_ignore_ascii_case("wal") {
2461                            wal = true;
2462                        } else if opt.eq_ignore_ascii_case("summary") {
2463                            // v7.39 (round 227) — `SUMMARY [ON|OFF]` really
2464                            // gates the trailing Planning/Execution Time
2465                            // lines now (was accept-and-no-op).
2466                            let value = match self.peek().clone() {
2467                                Token::On => {
2468                                    self.advance();
2469                                    true
2470                                }
2471                                Token::Ident(v) | Token::QuotedIdent(v)
2472                                    if v.eq_ignore_ascii_case("off") =>
2473                                {
2474                                    self.advance();
2475                                    false
2476                                }
2477                                Token::Ident(v) | Token::QuotedIdent(v)
2478                                    if v.eq_ignore_ascii_case("true") =>
2479                                {
2480                                    self.advance();
2481                                    true
2482                                }
2483                                _ => true,
2484                            };
2485                            summary_off = !value;
2486                        } else if opt.eq_ignore_ascii_case("verbose")
2487                            || opt.eq_ignore_ascii_case("format")
2488                        {
2489                            // v7.37.22 — accept-but-no-op the remaining
2490                            // PG options so EXPLAIN-using clients
2491                            // (pgAdmin / DataGrip) don't see syntax
2492                            // errors. FORMAT takes a value (text /
2493                            // json / yaml / xml); skip the next token
2494                            // if it's an ident.
2495                            if opt.eq_ignore_ascii_case("format") {
2496                                if let Token::Ident(v) | Token::QuotedIdent(v) = self.peek().clone()
2497                                {
2498                                    self.advance();
2499                                    format = match v.to_ascii_lowercase().as_str() {
2500                                        "text" => crate::ast::ExplainFormat::Text,
2501                                        "json" => crate::ast::ExplainFormat::Json,
2502                                        "xml" => crate::ast::ExplainFormat::Xml,
2503                                        "yaml" => crate::ast::ExplainFormat::Yaml,
2504                                        other => {
2505                                            return Err(self.err(format!(
2506                                                "EXPLAIN (FORMAT …): unknown format {other:?}; \
2507                                                 supports text, json, xml, yaml"
2508                                            )));
2509                                        }
2510                                    };
2511                                }
2512                            } else {
2513                                // VERBOSE / SUMMARY take optional ON/OFF;
2514                                // consume if present.
2515                                if matches!(self.peek(), Token::On) {
2516                                    self.advance();
2517                                } else if let Token::Ident(v) | Token::QuotedIdent(v) =
2518                                    self.peek().clone()
2519                                    && (v.eq_ignore_ascii_case("off")
2520                                        || v.eq_ignore_ascii_case("true"))
2521                                {
2522                                    self.advance();
2523                                    let _ = v;
2524                                }
2525                            }
2526                        } else {
2527                            return Err(self.err(format!(
2528                                "unknown EXPLAIN option {opt:?}; supports ANALYZE, COSTS, BUFFERS, TIMING, SETTINGS, WAL, SUGGEST, VERBOSE, FORMAT, SUMMARY"
2529                            )));
2530                        }
2531                        if matches!(self.peek(), Token::Comma) {
2532                            self.advance();
2533                            continue;
2534                        }
2535                        break;
2536                    }
2537                    if !matches!(self.peek(), Token::RParen) {
2538                        return Err(self.err(format!(
2539                            "expected ')' after EXPLAIN options, got {:?}",
2540                            self.peek()
2541                        )));
2542                    }
2543                    self.advance();
2544                } else if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
2545                    && (s.eq_ignore_ascii_case("analyze") || s.eq_ignore_ascii_case("analyse"))
2546                {
2547                    self.advance();
2548                    analyze = true;
2549                }
2550                // v7.39 (round 224) — the body may open with WITH (CTEs);
2551                // route through the same CTE-then-SELECT path the top-level
2552                // WITH statement uses. v7.39 (round 225) — DML bodies parse
2553                // too (PG explains INSERT / UPDATE / DELETE).
2554                let inner = match self.peek().clone() {
2555                    Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
2556                        self.advance();
2557                        self.parse_with_cte_then_select()?
2558                    }
2559                    Token::Insert => self.parse_insert_stmt(false)?,
2560                    Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
2561                        self.advance();
2562                        self.parse_update_after_keyword()?
2563                    }
2564                    Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
2565                        self.advance();
2566                        self.parse_delete_after_keyword()?
2567                    }
2568                    _ => self.parse_select_stmt()?,
2569                };
2570                if !matches!(
2571                    inner,
2572                    Statement::Select(_)
2573                        | Statement::Insert(_)
2574                        | Statement::Update(_)
2575                        | Statement::Delete(_)
2576                ) {
2577                    return Err(self.err(format!(
2578                        "EXPLAIN body must be SELECT / INSERT / UPDATE / DELETE, got {inner:?}"
2579                    )));
2580                }
2581                Ok(Statement::Explain(crate::ast::ExplainStatement {
2582                    analyze,
2583                    inner: Box::new(inner),
2584                    suggest,
2585                    costs_off,
2586                    buffers,
2587                    timing_off,
2588                    settings,
2589                    wal,
2590                    summary_off,
2591                    format,
2592                }))
2593            }
2594            Token::Create => self.parse_create_stmt(),
2595            Token::Insert => self.parse_insert_stmt(false),
2596            // MySQL `DESCRIBE t` / `DESC t` — the SHOW COLUMNS
2597            // spelling; route to the same handler. DESC is the
2598            // reserved ORDER BY token, so it gets its own arm.
2599            Token::Ident(s)
2600                if s.eq_ignore_ascii_case("describe")
2601                    && matches!(
2602                        self.tokens.get(self.pos + 1),
2603                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2604                    ) =>
2605            {
2606                self.advance();
2607                let table = self.expect_ident_like()?;
2608                Ok(Statement::ShowColumns(table))
2609            }
2610            Token::Desc
2611                if matches!(
2612                    self.tokens.get(self.pos + 1),
2613                    Some(Token::Ident(_) | Token::QuotedIdent(_))
2614                ) =>
2615            {
2616                self.advance();
2617                let table = self.expect_ident_like()?;
2618                Ok(Statement::ShowColumns(table))
2619            }
2620            // `COPY table [(cols)] TO STDOUT` — the export half of
2621            // pg_dump's COPY pair (the FROM stdin half rides the
2622            // embed import path). Options need a format design and
2623            // error honestly.
2624            Token::Ident(s)
2625                if s.eq_ignore_ascii_case("copy")
2626                    && matches!(
2627                        self.tokens.get(self.pos + 1),
2628                        Some(Token::Ident(_) | Token::QuotedIdent(_))
2629                    ) =>
2630            {
2631                self.advance(); // COPY
2632                let table = self.expect_ident_like()?;
2633                let columns = if matches!(self.peek(), Token::LParen) {
2634                    self.advance();
2635                    let mut cols = alloc::vec![self.expect_ident_like()?];
2636                    while matches!(self.peek(), Token::Comma) {
2637                        self.advance();
2638                        cols.push(self.expect_ident_like()?);
2639                    }
2640                    if !matches!(self.peek(), Token::RParen) {
2641                        return Err(self.err(format!(
2642                            "expected ')' after COPY column list, got {:?}",
2643                            self.peek()
2644                        )));
2645                    }
2646                    self.advance();
2647                    Some(cols)
2648                } else {
2649                    None
2650                };
2651                // v7.39 (round 249) — `COPY t FROM '<path>'`: the file
2652                // endpoint. (FROM STDIN still rides the wire/import path —
2653                // its data arrives out of band.)
2654                if matches!(self.peek(), Token::From)
2655                    && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)))
2656                {
2657                    self.advance(); // FROM
2658                    let Token::String(path) = self.advance() else {
2659                        unreachable!()
2660                    };
2661                    let options = self.parse_copy_to_options()?;
2662                    return Ok(Statement::CopyFromFile {
2663                        table,
2664                        columns,
2665                        path,
2666                        options,
2667                    });
2668                }
2669                if !matches!(self.peek(), Token::To) {
2670                    return Err(self.err(format!(
2671                        "COPY: only TO STDOUT is supported here (FROM stdin \
2672                         rides the import path); got {:?}",
2673                        self.peek()
2674                    )));
2675                }
2676                self.advance();
2677                if matches!(self.peek(), Token::String(_)) {
2678                    let Token::String(path) = self.advance() else { unreachable!() };
2679                    let options = self.parse_copy_to_options()?;
2680                    return Ok(Statement::CopyToFile {
2681                        table,
2682                        columns,
2683                        query: None,
2684                        path,
2685                        options,
2686                    });
2687                }
2688                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2689                    return Err(self.err(format!(
2690                        "COPY TO supports STDOUT only (no file endpoints), got {:?}",
2691                        self.peek()
2692                    )));
2693                }
2694                self.advance();
2695                let options = self.parse_copy_to_options()?;
2696                Ok(Statement::CopyTo {
2697                    table,
2698                    columns,
2699                    query: None,
2700                    options,
2701                })
2702            }
2703            // v7.39 (read01 round 94) — `COPY (<query>) TO STDOUT [WITH (…)]`.
2704            // The parens directly after COPY wrap a SELECT/VALUES/CTE whose
2705            // result set is streamed in COPY format (PG's query form).
2706            Token::Ident(s)
2707                if s.eq_ignore_ascii_case("copy")
2708                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
2709            {
2710                self.advance(); // COPY
2711                self.advance(); // (
2712                let query = self.parse_select_stmt()?;
2713                if !matches!(self.peek(), Token::RParen) {
2714                    return Err(self.err(format!(
2715                        "expected ')' after COPY query, got {:?}",
2716                        self.peek()
2717                    )));
2718                }
2719                self.advance(); // )
2720                if !matches!(self.peek(), Token::To) {
2721                    return Err(self.err(format!(
2722                        "COPY (query): only TO STDOUT is supported, got {:?}",
2723                        self.peek()
2724                    )));
2725                }
2726                self.advance();
2727                if matches!(self.peek(), Token::String(_)) {
2728                    let Token::String(path) = self.advance() else { unreachable!() };
2729                    let options = self.parse_copy_to_options()?;
2730                    return Ok(Statement::CopyToFile {
2731                        table: String::new(),
2732                        columns: None,
2733                        query: Some(alloc::boxed::Box::new(query)),
2734                        path,
2735                        options,
2736                    });
2737                }
2738                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("stdout")) {
2739                    return Err(self.err(format!(
2740                        "COPY (query): TO supports STDOUT only, got {:?}",
2741                        self.peek()
2742                    )));
2743                }
2744                self.advance();
2745                let options = self.parse_copy_to_options()?;
2746                Ok(Statement::CopyTo {
2747                    table: String::new(),
2748                    columns: None,
2749                    query: Some(alloc::boxed::Box::new(query)),
2750                    options,
2751                })
2752            }
2753            // MySQL `REPLACE INTO t …` — delete-then-insert upsert.
2754            // Shares the INSERT body; the replace flag lowers it
2755            // onto ON CONFLICT DO UPDATE with an empty assignment
2756            // list (engine: replace the whole row).
2757            Token::Ident(s)
2758                if s.eq_ignore_ascii_case("replace")
2759                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Into)) =>
2760            {
2761                self.parse_insert_stmt(true)
2762            }
2763            Token::Begin => {
2764                self.advance();
2765                // v7.38 轴 4 / v7.39 (read01 round 118, B3) — PG-standard
2766                // `BEGIN [WORK|TRANSACTION] [ISOLATION LEVEL …] [READ ONLY|WRITE]
2767                // [[NOT] DEFERRABLE]`. The optional WORK/TRANSACTION noise word
2768                // is consumed first, then the trailing modes — including the
2769                // case where `ISOLATION LEVEL …` follows BEGIN directly (no
2770                // WORK/TRANSACTION). The explicit level, when present, rides the
2771                // statement so `exec_begin` applies it for this transaction.
2772                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("work") || s.eq_ignore_ascii_case("transaction"))
2773                {
2774                    self.advance();
2775                }
2776                let iso = self.parse_isolation_level_clauses()?;
2777                Ok(Statement::Begin(iso))
2778            }
2779            // v7.38 轴 4 — PG-standard `START TRANSACTION …` synonym
2780            // for BEGIN. START is contextual in PG too; pattern-match
2781            // on the ident here. Iso clauses are parse-and-ignored,
2782            // same as BEGIN above.
2783            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("start") => {
2784                self.advance();
2785                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
2786                {
2787                    return Err(self.err(alloc::format!(
2788                        "expected TRANSACTION after START, got {:?}",
2789                        self.peek()
2790                    )));
2791                }
2792                self.advance();
2793                let iso = self.parse_isolation_level_clauses()?;
2794                Ok(Statement::Begin(iso))
2795            }
2796            Token::Commit => {
2797                self.advance();
2798                // PG: `COMMIT [WORK | TRANSACTION]`.
2799                if let Token::Ident(w) = self.peek()
2800                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2801                {
2802                    self.advance();
2803                }
2804                Ok(Statement::Commit)
2805            }
2806            // r1066 (7.38 S5.1) — `END [WORK | TRANSACTION]` is PG's
2807            // COMMIT synonym; pgbench's builtin tpcb-like script closes
2808            // every transaction with `END;` and the drop-in aborted on
2809            // it. Only reachable at statement start (CASE … END lives
2810            // inside expressions), so no ambiguity.
2811            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
2812                self.advance();
2813                if let Token::Ident(w) = self.peek()
2814                    && (w.eq_ignore_ascii_case("work") || w.eq_ignore_ascii_case("transaction"))
2815                {
2816                    self.advance();
2817                }
2818                Ok(Statement::Commit)
2819            }
2820            Token::Rollback => {
2821                self.advance();
2822                // `ROLLBACK TO [SAVEPOINT] <name>` returns to that
2823                // savepoint without ending the transaction. Bare
2824                // `ROLLBACK` drops the whole TX.
2825                if matches!(self.peek(), Token::To) {
2826                    self.advance();
2827                    if matches!(self.peek(), Token::Savepoint) {
2828                        self.advance();
2829                    }
2830                    let name = self.expect_ident_like()?;
2831                    Ok(Statement::RollbackToSavepoint(name))
2832                } else {
2833                    Ok(Statement::Rollback)
2834                }
2835            }
2836            Token::Savepoint => {
2837                self.advance();
2838                let name = self.expect_ident_like()?;
2839                Ok(Statement::Savepoint(name))
2840            }
2841            Token::Release => {
2842                self.advance();
2843                // `RELEASE [SAVEPOINT] <name>` — the `SAVEPOINT` keyword
2844                // is optional in standard SQL.
2845                if matches!(self.peek(), Token::Savepoint) {
2846                    self.advance();
2847                }
2848                let name = self.expect_ident_like()?;
2849                Ok(Statement::ReleaseSavepoint(name))
2850            }
2851            Token::Show => {
2852                self.advance();
2853                // `SHOW TABLES` / `SHOW USERS` / `SHOW COLUMNS FROM <table>`.
2854                // v6.1.2 promoted TABLES to a reserved keyword (for
2855                // `CREATE PUBLICATION … FOR ALL TABLES`), so it now
2856                // arrives as `Token::Tables` rather than a bare ident.
2857                // USERS / COLUMNS remain bare idents.
2858                let target = match self.advance() {
2859                    Token::Tables => "tables".to_string(),
2860                    // v7.17.0 Phase 3.P0-59 — CREATE is a reserved
2861                    // keyword token; recognise it as the SHOW CREATE
2862                    // dispatch keyword too.
2863                    Token::Create => "create".to_string(),
2864                    // v7.17.0 Phase 3.P0-60 — INDEX is a reserved
2865                    // keyword too; let SHOW INDEX FROM parse.
2866                    Token::Index => "index".to_string(),
2867                    // v7.37.17 (17.6 sibling) — SHOW ALL. ALL is
2868                    // reserved (used in aggregate function calls);
2869                    // recognise it here so the parser dispatches
2870                    // to ShowParameter("all") — the engine returns
2871                    // the curated parameter inventory.
2872                    Token::All => "all".to_string(),
2873                    // v7.38.18 (C12) — `SHOW COUNT(*) WARNINGS`, MySQL's
2874                    // spelling for the size of the diagnostics area.
2875                    // MySQL-dialect only: PostgreSQL 18.4 answers this
2876                    // phrase with `syntax error at or near "("`, and a
2877                    // PG session must keep getting exactly that rather
2878                    // than a message about an unknown parameter.
2879                    // `COUNT` arrives as a bare ident; the `(*)` and the
2880                    // trailing keyword are consumed here so the whole
2881                    // form reaches the engine as one parameter name.
2882                    Token::Ident(ref c)
2883                        if self.mysql_dialect
2884                            && c.eq_ignore_ascii_case("count")
2885                            && matches!(self.peek(), Token::LParen) =>
2886                    {
2887                        self.advance();
2888                        if matches!(self.peek(), Token::Star) {
2889                            self.advance();
2890                        }
2891                        if matches!(self.peek(), Token::RParen) {
2892                            self.advance();
2893                        }
2894                        match self.advance() {
2895                            Token::Ident(w) if w.eq_ignore_ascii_case("warnings") => {
2896                                return Ok(Statement::ShowParameter(
2897                                    "count(*) warnings".to_string(),
2898                                ));
2899                            }
2900                            other => {
2901                                return Err(self.err(format!(
2902                                    "expected WARNINGS after SHOW COUNT(*), got {other:?}"
2903                                )));
2904                            }
2905                        }
2906                    }
2907                    Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
2908                    other => {
2909                        return Err(self.err(format!(
2910                            "expected SHOW target, got {other:?}"
2911                        )));
2912                    }
2913                };
2914                match target.as_str() {
2915                    "tables" => Ok(Statement::ShowTables),
2916                    "users" => Ok(Statement::ShowUsers),
2917                    // v7.38 轴 4 — `SHOW transaction_isolation`
2918                    // returns the currently-selected isolation level.
2919                    "transaction_isolation" => Ok(Statement::ShowParameter(
2920                        "transaction_isolation".to_string(),
2921                    )),
2922                    // v7.17.0 Phase 3.P0-59 — MySQL `SHOW CREATE
2923                    // TABLE <t>` returns a 2-column row: (Table,
2924                    // Create Table). mysqldump emits this for every
2925                    // table at scrape time; without it the dump
2926                    // round-trip stalls.
2927                    // v7.17.0 Phase 3.P0-60 — MySQL `SHOW INDEXES
2928                    // FROM <t>` (also spelled `SHOW INDEX` and
2929                    // `SHOW KEYS`). admin / mysqldump probes use
2930                    // it to list per-table indexes.
2931                    "indexes" | "index" | "keys" => {
2932                        if !matches!(self.peek(), Token::From) {
2933                            return Err(self.err(format!(
2934                                "expected FROM after SHOW INDEXES, got {:?}",
2935                                self.peek()
2936                            )));
2937                        }
2938                        self.advance();
2939                        let table = self.expect_ident_like()?;
2940                        Ok(Statement::ShowIndexes(table))
2941                    }
2942                    // v7.17.0 Phase 3.P0-61 — MySQL `SHOW STATUS` /
2943                    // `SHOW VARIABLES`. Both return a 2-column row
2944                    // set listing server-side state; clients probe
2945                    // them at connect time.
2946                    "status" => Ok(Statement::ShowStatus),
2947                    "variables" => {
2948                        // r1067 — `SHOW VARIABLES LIKE 'pat'`.
2949                        if matches!(self.peek(), Token::Like) {
2950                            self.advance();
2951                            let pat = match self.advance() {
2952                                Token::String(p) => p,
2953                                other => {
2954                                    return Err(self.err(format!(
2955                                        "SHOW VARIABLES LIKE expects a quoted pattern, got {other:?}"
2956                                    )));
2957                                }
2958                            };
2959                            return Ok(Statement::ShowVariablesLike(pat));
2960                        }
2961                        Ok(Statement::ShowVariables)
2962                    }
2963                    // v7.17.0 Phase 3.P0-62 — MySQL `SHOW PROCESSLIST`.
2964                    "processlist" => Ok(Statement::ShowProcesslist),
2965                    "create" => {
2966                        // SHOW CREATE TABLE / VIEW / DATABASE — only
2967                        // TABLE is supported in v7.17.
2968                        let kind = match self.advance() {
2969                            Token::Ident(s) | Token::QuotedIdent(s) => s,
2970                            Token::Table => "table".to_string(),
2971                            other => {
2972                                return Err(self.err(format!(
2973                                    "expected TABLE after SHOW CREATE, got {other:?}"
2974                                )));
2975                            }
2976                        };
2977                        if !kind.eq_ignore_ascii_case("table") {
2978                            return Err(self.err(format!(
2979                                "unsupported SHOW CREATE {kind:?}; v7.17 supports TABLE only"
2980                            )));
2981                        }
2982                        let name = self.expect_ident_like()?;
2983                        Ok(Statement::ShowCreateTable(name))
2984                    }
2985                    // v7.17.0 Phase 3.P0-58 — MySQL `SHOW DATABASES`
2986                    // (and `SHOW SCHEMAS` alias). The mysql client uses
2987                    // it to populate the database selector at connect
2988                    // time; without it `mysql -p` errors before the
2989                    // first user query.
2990                    "databases" | "schemas" => Ok(Statement::ShowDatabases),
2991                    // v6.1.3 — PUBLICATIONS plural is NOT a reserved
2992                    // keyword on its own; it lands here as a bare
2993                    // ident. Returning all publications + their
2994                    // scope summary.
2995                    "publications" => Ok(Statement::ShowPublications),
2996                    // v6.1.4 — same shape for SUBSCRIPTIONS plural.
2997                    "subscriptions" => Ok(Statement::ShowSubscriptions),
2998                    "columns" => {
2999                        if !matches!(self.peek(), Token::From) {
3000                            return Err(self.err(format!(
3001                                "expected FROM after SHOW COLUMNS, got {:?}",
3002                                self.peek()
3003                            )));
3004                        }
3005                        self.advance();
3006                        let table = self.expect_ident_like()?;
3007                        Ok(Statement::ShowColumns(table))
3008                    }
3009                    // v7.38 轴 4 surface — `SHOW <param>` for any
3010                    // remaining session / preset parameter name
3011                    // (server_version, search_path, client_encoding,
3012                    // …). The engine's ShowParameter handler does the
3013                    // dispatch; unrecognised names error there with
3014                    // a pointer to pg_settings, not at parse time —
3015                    // so a driver that issues `SHOW spam_setting`
3016                    // gets a clear runtime error instead of a
3017                    // confusing "unknown SHOW target".
3018                    // v7.40.11 — PG's own spelling of the isolation
3019                    // probe, which is what every driver sends and what
3020                    // its own documentation writes. `transaction` is a
3021                    // bare ident here, so the target matched and the two
3022                    // words after it did not: the statement ended at
3023                    // `transaction` and the parser reported
3024                    // `syntax error at or near "ISOLATION"`.
3025                    "transaction"
3026                        if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("isolation"))
3027                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("level")) =>
3028                    {
3029                        self.advance(); // ISOLATION
3030                        self.advance(); // LEVEL
3031                        Ok(Statement::ShowParameter(
3032                            "transaction_isolation".to_string(),
3033                        ))
3034                    }
3035                    other => {
3036                        // v7.38 (read01 P3.20) — a custom namespaced GUC
3037                        // (`SHOW app.foo`) arrives as `app` + `.` + `foo`;
3038                        // consume the dotted tail so it round-trips with
3039                        // `SET app.foo` / `current_setting('app.foo')`.
3040                        let mut full = other.to_string();
3041                        while matches!(self.peek(), Token::Dot) {
3042                            self.advance();
3043                            let seg = self.expect_ident_like()?;
3044                            full.push('.');
3045                            full.push_str(&seg.to_ascii_lowercase());
3046                        }
3047                        Ok(Statement::ShowParameter(full))
3048                    }
3049                }
3050            }
3051            // v6.1.2: `DROP` is now a reserved keyword (it dispatches
3052            // to DROP USER and DROP PUBLICATION today; DROP TABLE /
3053            // DROP INDEX are still SHOW-shaped admin ops). Pre-6.1.2
3054            // arrived as a bare ident; tokenising it dedicatedly
3055            // keeps the dispatch tree small.
3056            Token::Drop => {
3057                self.advance();
3058                match self.peek() {
3059                    // v7.37.17 (17.6 sibling) — DROP OWNED BY <role>
3060                    // [, ...] [CASCADE | RESTRICT]. pg_dumpall emits
3061                    // around DROP ROLE cleanup. SPG has no role-owner
3062                    // model, so consume to boundary as a no-op.
3063                    Token::Ident(s) | Token::QuotedIdent(s)
3064                        if s.eq_ignore_ascii_case("owned") =>
3065                    {
3066                        // v7.39 (round 696) — still a no-op (SPG has no
3067                        // role-owner model), but the ROLE is carried out so
3068                        // the engine can refuse one that does not exist,
3069                        // which is what PG18 does.
3070                        self.advance();
3071                        if self.peek_is_by() {
3072                            self.advance();
3073                        }
3074                        let names = self.take_comma_separated_names();
3075                        self.consume_until_statement_boundary();
3076                        Ok(Statement::ValidateOnly {
3077                            kind: crate::ast::ValidateOnlyKind::RoleName,
3078                            names,
3079                        })
3080                    }
3081                    // v7.39 (round 436) — MySQL's `DROP TEMPORARY TABLE t`.
3082                    // It drops only a TEMPORARY table, and name resolution
3083                    // already prefers the session's own, so the keyword is
3084                    // consumed and the ordinary DROP TABLE path runs.
3085                    Token::Ident(s) | Token::QuotedIdent(s)
3086                        if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
3087                    {
3088                        self.advance();
3089                        if !matches!(self.peek(), Token::Table) {
3090                            return Err(self.err(alloc::format!(
3091                                "expected TABLE after DROP TEMPORARY, got {:?}",
3092                                self.peek()
3093                            )));
3094                        }
3095                        self.parse_drop_table_after_keyword()
3096                    }
3097                    Token::Publication => {
3098                        self.advance();
3099                        // v7.39 (round 754, F31-B4) — the round-753
3100                        // audit probe tripped over the missing
3101                        // `IF EXISTS` here (syntax error).
3102                        let if_exists = self.consume_if_exists();
3103                        let name = self.expect_ident_or_string()?;
3104                        Ok(Statement::DropPublication { name, if_exists })
3105                    }
3106                    Token::Subscription => {
3107                        self.advance();
3108                        let if_exists = self.consume_if_exists();
3109                        let name = self.expect_ident_or_string()?;
3110                        Ok(Statement::DropSubscription { name, if_exists })
3111                    }
3112                    Token::Ident(s) | Token::QuotedIdent(s)
3113                        if s.eq_ignore_ascii_case("user") || s.eq_ignore_ascii_case("role") =>
3114                    {
3115                        self.advance();
3116                        // v7.39 (read01 round 58) — DROP ROLE is DROP USER: a
3117                        // login user IS a role in PG, and SPG's store holds
3118                        // both. `IF EXISTS` is accepted on either spelling.
3119                        let if_exists = self.consume_if_exists();
3120                        let name = self.expect_ident_or_string()?;
3121                        Ok(Statement::DropUser { name, if_exists })
3122                    }
3123                    // v7.39 (round 806) — DROP DATABASE [IF EXISTS] <name>.
3124                    // CREATE DATABASE has parsed since v7.14 and this did
3125                    // not, so `DROP DATABASE IF EXISTS x` — what every
3126                    // teardown script and pg_dumpall preamble opens with —
3127                    // came back as a syntax error, which IF EXISTS cannot
3128                    // soften. The name is carried so the engine can answer
3129                    // the way PG does; PG never lets this succeed on a
3130                    // single-database server, since the name is either
3131                    // unknown ("database … does not exist", or a notice
3132                    // under IF EXISTS) or the one you are connected to
3133                    // ("cannot drop the currently open database").
3134                    Token::Ident(s) | Token::QuotedIdent(s)
3135                        if s.eq_ignore_ascii_case("database") =>
3136                    {
3137                        self.advance();
3138                        let if_exists = self.consume_if_exists();
3139                        let name = self.expect_ident_or_string()?;
3140                        self.consume_until_statement_boundary();
3141                        Ok(Statement::DropDatabase { name, if_exists })
3142                    }
3143                    // v7.12.4 — DROP TRIGGER [IF EXISTS] name ON table.
3144                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
3145                        self.advance();
3146                        let if_exists = self.consume_if_exists();
3147                        let name = self.expect_ident_like()?;
3148                        // ON <table>
3149                        if !matches!(self.peek(), Token::On) {
3150                            return Err(self.err(alloc::format!(
3151                                "expected ON <table> after DROP TRIGGER {name:?}, got {:?}",
3152                                self.peek()
3153                            )));
3154                        }
3155                        self.advance();
3156                        let table = self.expect_ident_like()?;
3157                        Ok(Statement::DropTrigger {
3158                            name,
3159                            table,
3160                            if_exists,
3161                        })
3162                    }
3163                    // v7.39 (round 139) — DROP RULE [IF EXISTS] name ON table
3164                    // [CASCADE|RESTRICT]. Mirrors DROP TRIGGER's shape.
3165                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
3166                        self.advance();
3167                        let if_exists = self.consume_if_exists();
3168                        let name = self.expect_ident_like()?;
3169                        if !matches!(self.peek(), Token::On) {
3170                            return Err(self.err(alloc::format!(
3171                                "expected ON <table> after DROP RULE {name:?}, got {:?}",
3172                                self.peek()
3173                            )));
3174                        }
3175                        self.advance();
3176                        let table = self.expect_ident_like()?;
3177                        // Optional CASCADE / RESTRICT — accepted, no effect.
3178                        self.consume_until_statement_boundary();
3179                        Ok(Statement::DropRule {
3180                            name,
3181                            table,
3182                            if_exists,
3183                        })
3184                    }
3185                    // v7.12.4 — DROP FUNCTION [IF EXISTS] name [(args)].
3186                    // v7.12.4 ignores any optional arg-list (signature-
3187                    // based overload disambiguation lands in v7.12.5+).
3188                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
3189                        self.advance();
3190                        let if_exists = self.consume_if_exists();
3191                        let name = self.expect_ident_like()?;
3192                        // v7.39 (read01 round 62) — the argument list identifies
3193                        // WHICH overload to drop, so it is captured, not
3194                        // discarded. `DROP FUNCTION f` (no list) is legal when
3195                        // the name is unambiguous; the engine enforces that.
3196                        let args = if matches!(self.peek(), Token::LParen) {
3197                            Some(self.parse_function_signature_types()?)
3198                        } else {
3199                            None
3200                        };
3201                        // v7.39 (round 621) — the `CASCADE` / `RESTRICT`
3202                        // trailer, which `DROP TABLE` and `DROP INDEX` have
3203                        // accepted since v7.14 and this one refused outright.
3204                        // pg_dump writes it, so refusing was a parse error in
3205                        // the middle of a restore. SPG drops the function
3206                        // either way — it tracks no dependents to cascade to —
3207                        // which is the same reading the other two give it.
3208                        self.consume_drop_behaviour();
3209                        Ok(Statement::DropFunction {
3210                            name,
3211                            args,
3212                            if_exists,
3213                        })
3214                    }
3215                    // v7.14.0 — DROP TABLE [IF EXISTS] name [, name…]
3216                    // [CASCADE|RESTRICT]. pg_dump and mysqldump both
3217                    // emit DROP TABLE IF EXISTS at the head of every
3218                    // CREATE TABLE block so re-importing a dump
3219                    // overwrites prior state. SPG accepts and removes
3220                    // matching tables; CASCADE/RESTRICT trailers
3221                    // accepted silently.
3222                    Token::Table => self.parse_drop_table_after_keyword(),
3223                    // v7.14.0 — DROP INDEX [IF EXISTS] name
3224                    // [CASCADE|RESTRICT]. PG / mysqldump emit this
3225                    // for partial-index renames and pgvector
3226                    // migrations. SPG removes the matching index;
3227                    // IF EXISTS makes the drop idempotent.
3228                    Token::Index => {
3229                        self.advance();
3230                        let if_exists_at = self.pos;
3231                        let if_exists = self.consume_if_exists();
3232                        let name = self.expect_ident_like()?;
3233                        // v7.39.7 — MySQL's own spelling, which SPG
3234                        // refused.
3235                        //
3236                        // `DROP INDEX i ON t` is how MySQL drops an
3237                        // index; its names live inside a table, so the
3238                        // statement names the table. Measured against
3239                        // MySQL 9.7.2: the form above works, and the
3240                        // bare `DROP INDEX i` PostgreSQL uses is a 1064
3241                        // there. SPG had it exactly backwards on the
3242                        // MySQL wire — the bare form accepted, MySQL's
3243                        // own a syntax error — so a migration that drops
3244                        // an index failed against the drop-in and not
3245                        // against the thing it replaces.
3246                        let table = if matches!(self.peek(), Token::On) {
3247                            self.advance();
3248                            Some(self.expect_ident_like()?)
3249                        } else {
3250                            None
3251                        };
3252                        if self.mysql_dialect {
3253                            // MySQL has no `IF EXISTS` here either:
3254                            // `DROP INDEX IF EXISTS i ON t` is a 1064.
3255                            if if_exists {
3256                                return Err(self.err_at(
3257                                    if_exists_at,
3258                                    "MySQL has no IF EXISTS on DROP INDEX".into(),
3259                                ));
3260                            }
3261                            if table.is_none() {
3262                                return Err(self.err("expected ON after the index name".into()));
3263                            }
3264                        }
3265                        if matches!(
3266                            self.peek(),
3267                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3268                                || s.eq_ignore_ascii_case("restrict")
3269                        ) {
3270                            self.advance();
3271                        }
3272                        Ok(Statement::DropIndex {
3273                            name,
3274                            if_exists,
3275                            table,
3276                        })
3277                    }
3278                    // v7.14.0 — DROP SCHEMA [IF EXISTS] name
3279                    // [CASCADE|RESTRICT]. SPG is single-database;
3280                    // v7.17.0 Phase 1.6 — DROP SCHEMA [IF EXISTS]
3281                    // name [, name…] [CASCADE | RESTRICT]. Real
3282                    // unregister (was silent no-op pre-v7.17).
3283                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
3284                        self.advance();
3285                        let if_exists = self.consume_if_exists();
3286                        let mut names = vec![self.expect_ident_like()?];
3287                        while matches!(self.peek(), Token::Comma) {
3288                            self.advance();
3289                            names.push(self.expect_ident_like()?);
3290                        }
3291                        if matches!(
3292                            self.peek(),
3293                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3294                                || s.eq_ignore_ascii_case("restrict")
3295                        ) {
3296                            self.advance();
3297                        }
3298                        Ok(Statement::DropSchema { names, if_exists })
3299                    }
3300                    // v7.17.0 Phase 1.4 — DROP TYPE [IF EXISTS]
3301                    // name [, name…] [CASCADE|RESTRICT].
3302                    Token::Ident(s) | Token::QuotedIdent(s)
3303                        if s.eq_ignore_ascii_case("type") =>
3304                    {
3305                        self.advance();
3306                        let if_exists = self.consume_if_exists();
3307                        let mut names = vec![self.expect_ident_like()?];
3308                        while matches!(self.peek(), Token::Comma) {
3309                            self.advance();
3310                            names.push(self.expect_ident_like()?);
3311                        }
3312                        if matches!(
3313                            self.peek(),
3314                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3315                                || s.eq_ignore_ascii_case("restrict")
3316                        ) {
3317                            self.advance();
3318                        }
3319                        Ok(Statement::DropType { names, if_exists })
3320                    }
3321                    // v7.17.0 Phase 1.5 — DROP DOMAIN [IF EXISTS]
3322                    // name [, name…] [CASCADE|RESTRICT].
3323                    Token::Ident(s) | Token::QuotedIdent(s)
3324                        if s.eq_ignore_ascii_case("domain") =>
3325                    {
3326                        self.advance();
3327                        let if_exists = self.consume_if_exists();
3328                        let mut names = vec![self.expect_ident_like()?];
3329                        while matches!(self.peek(), Token::Comma) {
3330                            self.advance();
3331                            names.push(self.expect_ident_like()?);
3332                        }
3333                        if matches!(
3334                            self.peek(),
3335                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3336                                || s.eq_ignore_ascii_case("restrict")
3337                        ) {
3338                            self.advance();
3339                        }
3340                        Ok(Statement::DropDomain { names, if_exists })
3341                    }
3342                    // v7.17.0 Phase 1.3 — DROP MATERIALIZED VIEW
3343                    // [IF EXISTS] name [, name…] [CASCADE|RESTRICT].
3344                    Token::Ident(s) | Token::QuotedIdent(s)
3345                        if s.eq_ignore_ascii_case("materialized") =>
3346                    {
3347                        self.advance();
3348                        let nxt = self.peek().clone();
3349                        if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3350                        {
3351                            return Err(self.err(alloc::format!(
3352                                "expected VIEW after DROP MATERIALIZED, got {nxt:?}"
3353                            )));
3354                        }
3355                        self.advance();
3356                        let if_exists = self.consume_if_exists();
3357                        let mut names = vec![self.expect_ident_like()?];
3358                        while matches!(self.peek(), Token::Comma) {
3359                            self.advance();
3360                            names.push(self.expect_ident_like()?);
3361                        }
3362                        if matches!(
3363                            self.peek(),
3364                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3365                                || s.eq_ignore_ascii_case("restrict")
3366                        ) {
3367                            self.advance();
3368                        }
3369                        Ok(Statement::DropMaterializedView { names, if_exists })
3370                    }
3371                    // v7.17.0 Phase 1.2 — DROP VIEW [IF EXISTS]
3372                    // name [, name…] [CASCADE|RESTRICT].
3373                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
3374                        self.advance();
3375                        let if_exists = self.consume_if_exists();
3376                        let mut names = vec![self.expect_ident_like()?];
3377                        while matches!(self.peek(), Token::Comma) {
3378                            self.advance();
3379                            names.push(self.expect_ident_like()?);
3380                        }
3381                        if matches!(
3382                            self.peek(),
3383                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3384                                || s.eq_ignore_ascii_case("restrict")
3385                        ) {
3386                            self.advance();
3387                        }
3388                        Ok(Statement::DropView { names, if_exists })
3389                    }
3390                    // v7.17.0 — DROP SEQUENCE [IF EXISTS] name [,name…]
3391                    // [CASCADE|RESTRICT]. Real removal from catalog
3392                    // (was a silent no-op pre-v7.17).
3393                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
3394                        self.advance();
3395                        let if_exists = self.consume_if_exists();
3396                        let mut names = vec![self.expect_ident_like()?];
3397                        while matches!(self.peek(), Token::Comma) {
3398                            self.advance();
3399                            names.push(self.expect_ident_like()?);
3400                        }
3401                        if matches!(
3402                            self.peek(),
3403                            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
3404                                || s.eq_ignore_ascii_case("restrict")
3405                        ) {
3406                            self.advance();
3407                        }
3408                        Ok(Statement::DropSequence { names, if_exists })
3409                    }
3410                    // v7.39 (RLS) — DROP POLICY [IF EXISTS] name ON table.
3411                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
3412                        self.advance();
3413                        self.parse_drop_policy_after_keyword()
3414                    }
3415                    // v7.37.17 (17.6 siblings) — DROP <target> for
3416                    // targets SPG doesn't natively track. pg_dump
3417                    // emits DROP EXTENSION / DROP TYPE / DROP DOMAIN
3418                    // / DROP AGGREGATE / DROP OPERATOR / DROP CAST /
3419                    // DROP COLLATION / DROP LANGUAGE / DROP CONVERSION
3420                    // / DROP TEXT SEARCH / DROP FOREIGN * / DROP
3421                    // SERVER / DROP MATERIALIZED VIEW / DROP EVENT
3422                    // TRIGGER / DROP TABLESPACE / DROP RULE / DROP
3423                    // POLICY / DROP LARGE OBJECT / DROP ROLE / DROP
3424                    // ACCESS METHOD / DROP OPERATOR CLASS/FAMILY /
3425                    // etc. — accept + Empty-return so pg_dump tails
3426                    // load through. Materialized-view drop dispatches
3427                    // to the existing DropTable path when the token
3428                    // is Materialized-View-shaped (elsewhere in
3429                    // this parser).
3430                    Token::Ident(s) | Token::QuotedIdent(s)
3431                        if s.eq_ignore_ascii_case("text")
3432                            // The DROP dispatch matches on PEEK — `text` is
3433                            // not yet consumed, so SEARCH/CONFIGURATION sit
3434                            // at pos+1/pos+2 (the round-695 trap's mirror).
3435                            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("search"))
3436                            && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
3437                    {
3438                        // v7.39 (round 709) — DROP TEXT SEARCH CONFIGURATION
3439                        // validates the name; DICTIONARY / PARSER / TEMPLATE
3440                        // stay in the noise arm below.
3441                        self.advance(); // TEXT
3442                        self.advance(); // SEARCH
3443                        self.advance(); // CONFIGURATION
3444                        let if_exists = self.consume_if_exists();
3445                        let names = self.take_comma_separated_names();
3446                        self.consume_until_statement_boundary();
3447                        if if_exists {
3448                            return Ok(Statement::Empty);
3449                        }
3450                        Ok(Statement::ValidateOnly {
3451                            kind: crate::ast::ValidateOnlyKind::TsConfigName,
3452                            names,
3453                        })
3454                    }
3455                    Token::Ident(s) | Token::QuotedIdent(s)
3456                        if matches!(
3457                            s.to_ascii_lowercase().as_str(),
3458                            "type"
3459                                | "domain"
3460                                | "operator"
3461                                | "cast"
3462                                // `text` = TEXT SEARCH DICTIONARY / PARSER /
3463                                // TEMPLATE (CONFIGURATION intercepted above).
3464                                | "text"
3465                                | "materialized"
3466                                | "large"
3467                                | "role"
3468                                | "access"
3469                                | "procedure"
3470                                | "routine"
3471                        ) =>
3472                    {
3473                        self.consume_until_statement_boundary();
3474                        Ok(Statement::Empty)
3475                    }
3476                    // v7.39 (round 709) — DROP COLLATION / EVENT TRIGGER /
3477                    // TABLESPACE / TEXT SEARCH CONFIGURATION validate their
3478                    // NAME; DROP SERVER / DROP FOREIGN TABLE join the
3479                    // foreign-data warning family (round 706) so a
3480                    // CREATE→DROP sequence in a dump stays consistent.
3481                    Token::Ident(s) | Token::QuotedIdent(s)
3482                        if s.eq_ignore_ascii_case("server")
3483                            || s.eq_ignore_ascii_case("foreign") =>
3484                    {
3485                        self.advance();
3486                        self.consume_until_statement_boundary();
3487                        Ok(Statement::ValidateOnly {
3488                            kind: crate::ast::ValidateOnlyKind::ForeignInfra,
3489                            names: Vec::new(),
3490                        })
3491                    }
3492                    Token::Ident(s) | Token::QuotedIdent(s)
3493                        if s.eq_ignore_ascii_case("collation")
3494                            || s.eq_ignore_ascii_case("tablespace") =>
3495                    {
3496                        let kind = if s.eq_ignore_ascii_case("collation") {
3497                            crate::ast::ValidateOnlyKind::CollationName
3498                        } else {
3499                            crate::ast::ValidateOnlyKind::TablespaceName
3500                        };
3501                        self.advance();
3502                        let if_exists = self.consume_if_exists();
3503                        let names = self.take_comma_separated_names();
3504                        self.consume_until_statement_boundary();
3505                        if if_exists {
3506                            return Ok(Statement::Empty);
3507                        }
3508                        Ok(Statement::ValidateOnly { kind, names })
3509                    }
3510                    Token::Ident(s) | Token::QuotedIdent(s)
3511                        if s.eq_ignore_ascii_case("event") =>
3512                    {
3513                        self.advance();
3514                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger"))
3515                        {
3516                            self.advance();
3517                        }
3518                        let if_exists = self.consume_if_exists();
3519                        let names = self.take_comma_separated_names();
3520                        self.consume_until_statement_boundary();
3521                        if if_exists {
3522                            return Ok(Statement::Empty);
3523                        }
3524                        Ok(Statement::ValidateOnly {
3525                            kind: crate::ast::ValidateOnlyKind::EventTriggerName,
3526                            names,
3527                        })
3528                    }
3529                    // v7.39 (round 708) — DROP CONVERSION / DROP LANGUAGE
3530                    // leave the noise list; see the ValidateOnly kinds.
3531                    Token::Ident(s) | Token::QuotedIdent(s)
3532                        if s.eq_ignore_ascii_case("conversion")
3533                            || s.eq_ignore_ascii_case("language")
3534                            // `DROP PROCEDURAL LANGUAGE` puts the modifier
3535                            // FIRST — the first draft looked for it after.
3536                            || s.eq_ignore_ascii_case("procedural") =>
3537                    {
3538                        let kind = if s.eq_ignore_ascii_case("conversion") {
3539                            crate::ast::ValidateOnlyKind::ConversionName
3540                        } else {
3541                            crate::ast::ValidateOnlyKind::LanguageName
3542                        };
3543                        self.advance();
3544                        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("language"))
3545                        {
3546                            self.advance();
3547                        }
3548                        let if_exists = self.consume_if_exists();
3549                        let names = self.take_comma_separated_names();
3550                        self.consume_until_statement_boundary();
3551                        if if_exists {
3552                            return Ok(Statement::Empty);
3553                        }
3554                        Ok(Statement::ValidateOnly { kind, names })
3555                    }
3556                    // v7.39 (round 707) — `DROP AGGREGATE [IF EXISTS]
3557                    // name(argtypes)[, …]`. Parsed for real so the engine
3558                    // can answer as PG does; see Statement::DropAggregate.
3559                    Token::Ident(s) | Token::QuotedIdent(s)
3560                        if s.eq_ignore_ascii_case("aggregate") =>
3561                    {
3562                        self.advance();
3563                        let if_exists = self.consume_if_exists();
3564                        let mut items: Vec<(String, Option<Vec<String>>)> = Vec::new();
3565                        loop {
3566                            let name = self.expect_ident_like()?;
3567                            if !matches!(self.peek(), Token::LParen) {
3568                                return Err(self.err(alloc::format!(
3569                                    "expected argument list after DROP AGGREGATE {name}"
3570                                )));
3571                            }
3572                            self.advance();
3573                            let mut args: Vec<String> = Vec::new();
3574                            let mut star = false;
3575                            loop {
3576                                match self.peek().clone() {
3577                                    Token::RParen => {
3578                                        self.advance();
3579                                        break;
3580                                    }
3581                                    Token::Star => {
3582                                        self.advance();
3583                                        star = true;
3584                                    }
3585                                    Token::Comma => {
3586                                        self.advance();
3587                                    }
3588                                    _ => {
3589                                        // A type name may be multi-token
3590                                        // (`double precision`); glue idents
3591                                        // until , or ).
3592                                        let mut t = self.expect_ident_like()?;
3593                                        while let Token::Ident(nx) = self.peek() {
3594                                            let nx = nx.clone();
3595                                            self.advance();
3596                                            t.push(' ');
3597                                            t.push_str(&nx);
3598                                        }
3599                                        args.push(t);
3600                                    }
3601                                }
3602                            }
3603                            items.push((name, if star { None } else { Some(args) }));
3604                            if matches!(self.peek(), Token::Comma) {
3605                                self.advance();
3606                            } else {
3607                                break;
3608                            }
3609                        }
3610                        self.consume_until_statement_boundary();
3611                        Ok(Statement::DropAggregate { if_exists, items })
3612                    }
3613                    // v7.39 (round 697) — `DROP EXTENSION [IF EXISTS] <e>
3614                    // [, …] [CASCADE|RESTRICT]`. PG refuses one that is not
3615                    // installed; `IF EXISTS` is the spelling that says do
3616                    // not, and it keeps the no-op.
3617                    Token::Ident(s) | Token::QuotedIdent(s)
3618                        if s.eq_ignore_ascii_case("extension") =>
3619                    {
3620                        self.advance();
3621                        let if_exists = self.consume_if_exists();
3622                        let names = self.take_comma_separated_names();
3623                        self.consume_until_statement_boundary();
3624                        if if_exists {
3625                            return Ok(Statement::Empty);
3626                        }
3627                        Ok(Statement::ValidateOnly {
3628                            kind: crate::ast::ValidateOnlyKind::ExtensionInstalled,
3629                            names,
3630                        })
3631                    }
3632                    Token::Ident(s) | Token::QuotedIdent(s)
3633                        if s.eq_ignore_ascii_case("statistics") =>
3634                    {
3635                        self.parse_drop_statistics_after_drop()
3636                    }
3637                    other => Err(self.err(format!(
3638                        "expected TABLE / INDEX / SCHEMA / SEQUENCE / USER / PUBLICATION / \
3639                         SUBSCRIPTION / TRIGGER / FUNCTION / STATISTICS after DROP, got {other:?}"
3640                    ))),
3641                }
3642            }
3643            // v7.17.0 Phase 1.3 — REFRESH MATERIALIZED VIEW name [WITH [NO] DATA].
3644            // v7.37.19 (19.8) — `CONCURRENTLY` modifier (PG 9.4+) parsed
3645            // and accepted before the view name. SPG materialised
3646            // views re-evaluate on read (always-fresh semantics), so
3647            // the CONCURRENTLY-vs-serial distinction has no runtime
3648            // effect — the refresh body does not block readers either
3649            // way. Same accept-and-no-op pattern as DETACH PARTITION
3650            // CONCURRENTLY (16.5).
3651            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("refresh") => {
3652                self.advance();
3653                let nxt = self.peek().clone();
3654                if !matches!(&nxt, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("materialized"))
3655                {
3656                    return Err(self.err(alloc::format!(
3657                        "expected MATERIALIZED after REFRESH, got {nxt:?}"
3658                    )));
3659                }
3660                self.advance();
3661                let nxt2 = self.peek().clone();
3662                if !matches!(&nxt2, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
3663                {
3664                    return Err(self.err(alloc::format!(
3665                        "expected VIEW after REFRESH MATERIALIZED, got {nxt2:?}"
3666                    )));
3667                }
3668                self.advance();
3669                // Optional CONCURRENTLY noise word — consumed without
3670                // changing semantics.
3671                if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("concurrently"))
3672                {
3673                    self.advance();
3674                }
3675                let name = self.expect_ident_like()?;
3676                let with_data = self.parse_optional_with_data(true)?;
3677                Ok(Statement::RefreshMaterializedView { name, with_data })
3678            }
3679            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
3680                self.advance();
3681                self.parse_update_after_keyword()
3682            }
3683            // v7.37.17 (17.6 sibling) — TRUNCATE [TABLE] [ONLY]
3684            // <name> [, ...] [RESTART IDENTITY | CONTINUE IDENTITY]
3685            // [CASCADE | RESTRICT]. Clears every row from each named
3686            // table. Parses at the top level; the engine dispatcher
3687            // walks Statement::Truncate.
3688            // v7.39.9 — MySQL's top-level `RENAME TABLE a TO b [, c TO d]`.
3689            //
3690            // PostgreSQL renames a table through `ALTER TABLE … RENAME
3691            // TO`, which SPG already had, so this spelling answered 1064
3692            // — and it is what a MySQL migration writes. Measured on
3693            // 9.7.2: several pairs in one statement are accepted, and
3694            // renaming onto a name that exists is 1050.
3695            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename") => {
3696                self.advance();
3697                if matches!(self.peek(), Token::Table) {
3698                    self.advance();
3699                }
3700                let mut pairs: Vec<(String, String)> = Vec::new();
3701                loop {
3702                    let from = self.expect_ident_like()?;
3703                    if matches!(self.peek(), Token::To) {
3704                        self.advance();
3705                    } else {
3706                        self.expect_keyword_ident("to")?;
3707                    }
3708                    let to = self.expect_ident_like()?;
3709                    pairs.push((from, to));
3710                    if matches!(self.peek(), Token::Comma) {
3711                        self.advance();
3712                    } else {
3713                        break;
3714                    }
3715                }
3716                Ok(Statement::RenameTables(pairs))
3717            }
3718            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
3719                self.advance();
3720                // Optional TABLE noise word — PG accepts both the reserved
3721                // token and the bare identifier spelling.
3722                if matches!(self.peek(), Token::Table)
3723                    || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table"))
3724                {
3725                    self.advance();
3726                }
3727                // v7.39 (round 647) — `TRUNCATE ONLY t` is carried now,
3728                // not absorbed. The lookahead keeps a table genuinely
3729                // called `only` working: the keyword is a keyword only
3730                // when a name follows it.
3731                let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
3732                    if s.eq_ignore_ascii_case("only"))
3733                    && matches!(
3734                        self.tokens.get(self.pos + 1),
3735                        Some(Token::Ident(_) | Token::QuotedIdent(_))
3736                    );
3737                if only {
3738                    self.advance();
3739                }
3740                // Table names (comma-separated).
3741                let mut tables = Vec::new();
3742                loop {
3743                    tables.push(self.expect_ident_like()?);
3744                    if matches!(self.peek(), Token::Comma) {
3745                        self.advance();
3746                        continue;
3747                    }
3748                    break;
3749                }
3750                // Optional RESTART IDENTITY / CONTINUE IDENTITY.
3751                let mut restart_identity = false;
3752                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restart"))
3753                {
3754                    self.advance();
3755                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3756                    {
3757                        self.advance();
3758                        restart_identity = true;
3759                    }
3760                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
3761                {
3762                    self.advance();
3763                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("identity"))
3764                    {
3765                        self.advance();
3766                    }
3767                }
3768                // Optional CASCADE / RESTRICT.
3769                let mut cascade = false;
3770                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cascade"))
3771                {
3772                    self.advance();
3773                    cascade = true;
3774                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("restrict"))
3775                {
3776                    self.advance();
3777                }
3778                Ok(Statement::Truncate {
3779                    tables,
3780                    restart_identity,
3781                    cascade,
3782                    only,
3783                })
3784            }
3785            // v7.37.17 (17.6 sibling) — REINDEX [(OPTION [, ...])]
3786            // [CONCURRENTLY] { INDEX | TABLE | SCHEMA | DATABASE |
3787            // SYSTEM } [IF EXISTS] <name>. SPG rebuilds indexes as
3788            // rows change so the index tree is always up-to-date;
3789            // REINDEX is a strict no-op. Accept the whole statement
3790            // shape to boundary for pg_dump round-trip compatibility.
3791            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reindex") => {
3792                // v7.39 (round 535) — the target is CARRIED now. SPG has no
3793                // index bloat to rebuild, so the work stays a no-op, but PG
3794                // validates what it was pointed at and this swallowed the
3795                // name at parse time — `REINDEX TABLE typo` reported
3796                // success. Measured on PG18: INDEX / TABLE name a relation,
3797                // SCHEMA a schema, SYSTEM nothing.
3798                self.advance();
3799                self.parse_reindex_tail()
3800            }
3801            // v7.37.17 (17.6 sibling) — VACUUM [(OPTION [, ...])]
3802            // [FULL] [FREEZE] [VERBOSE] [ANALYZE] [<table> [(cols)]].
3803            // SPG has no MVCC bloat today (Phase D visibility map
3804            // queues with v7.38); the freezer collapses hot-tier
3805            // rows into cold segments automatically. VACUUM is a
3806            // no-op — pg_dump maintenance scripts and Discourse's
3807            // periodic-maintenance path both emit it.
3808            // v7.39 (round 169) — VACUUM is REAL now: with the in-place
3809            // MVCC gate ON (v7.37.15 flip), tombstoned versions are
3810            // actual bloat, so the pre-MVCC accept-and-ignore posture
3811            // became a silent no-op on a customer's manual reclaim.
3812            // Grammar: VACUUM [(opts)] [FULL] [FREEZE] [VERBOSE]
3813            // [ANALYZE] [<table> [(cols)]] — option words are absorbed,
3814            // ANALYZE is captured, the optional table name is captured.
3815            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("vacuum") => {
3816                self.advance();
3817                // Parenthesised option list: absorb it.
3818                if matches!(self.peek(), Token::LParen) {
3819                    let mut depth = 0usize;
3820                    loop {
3821                        match self.advance() {
3822                            Token::LParen => depth += 1,
3823                            Token::RParen => {
3824                                depth -= 1;
3825                                if depth == 0 {
3826                                    break;
3827                                }
3828                            }
3829                            Token::Eof => break,
3830                            _ => {}
3831                        }
3832                    }
3833                }
3834                let mut analyze = false;
3835                let mut table: Option<String> = None;
3836                loop {
3837                    match self.peek() {
3838                        // v7.39 (round 535) — `FULL` lexes as a keyword, not
3839                        // an identifier, so the loop below broke out on it and
3840                        // dropped the table name: `VACUUM FULL nosuch` was
3841                        // accepted where `VACUUM nosuch` was refused.
3842                        Token::Full => {
3843                            self.advance();
3844                        }
3845                        Token::Ident(w) | Token::QuotedIdent(w) => {
3846                            let wl = w.to_ascii_lowercase();
3847                            match wl.as_str() {
3848                                "full" | "freeze" | "verbose" => {
3849                                    self.advance();
3850                                }
3851                                "analyze" | "analyse" => {
3852                                    analyze = true;
3853                                    self.advance();
3854                                }
3855                                _ => {
3856                                    table = Some(self.expect_ident_like()?);
3857                                    break;
3858                                }
3859                            }
3860                        }
3861                        _ => break,
3862                    }
3863                }
3864                // Optional trailing column list / anything else to the
3865                // statement boundary (PG accepts per-column ANALYZE).
3866                self.consume_until_statement_boundary();
3867                Ok(Statement::Vacuum { table, analyze })
3868            }
3869            // v7.37.17 (17.6 sibling) — CLUSTER [VERBOSE] <table>
3870            // [USING <index>] / CLUSTER (VERBOSE) <table> USING
3871            // <index>. PG stores rows in physical order matching
3872            // an index; SPG's hot-tier is append-only + cold-tier
3873            // is segment-frozen, so clustering has no persistent
3874            // effect. Accept-and-no-op for pg_dump compat.
3875            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("cluster") => {
3876                // v7.39 (round 535) — same as REINDEX above: the relation is
3877                // carried so the engine can refuse one that does not exist.
3878                // A bare `CLUSTER [VERBOSE]` names nothing and is accepted.
3879                self.advance();
3880                self.parse_cluster_tail()
3881            }
3882            // v7.39 (round 222) — LISTEN / NOTIFY / UNLISTEN with real
3883            // delivery (was accept-and-drop since v7.37.17). NOTIFY takes an
3884            // optional string payload; UNLISTEN takes a channel or `*`.
3885            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("listen") => {
3886                self.advance();
3887                let ch = match self.advance() {
3888                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3889                    other => {
3890                        return Err(self.err(format!(
3891                            "expected channel name after LISTEN, got {other:?}"
3892                        )));
3893                    }
3894                };
3895                Ok(Statement::Listen(ch))
3896            }
3897            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("notify") => {
3898                self.advance();
3899                let channel = match self.advance() {
3900                    Token::Ident(c) | Token::QuotedIdent(c) => c,
3901                    other => {
3902                        return Err(self.err(format!(
3903                            "expected channel name after NOTIFY, got {other:?}"
3904                        )));
3905                    }
3906                };
3907                let payload = if matches!(self.peek(), Token::Comma) {
3908                    self.advance();
3909                    match self.advance() {
3910                        Token::String(p) => Some(p),
3911                        other => {
3912                            return Err(self.err(format!(
3913                                "expected string payload after NOTIFY <channel>, got {other:?}"
3914                            )));
3915                        }
3916                    }
3917                } else {
3918                    None
3919                };
3920                Ok(Statement::Notify { channel, payload })
3921            }
3922            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlisten") => {
3923                self.advance();
3924                match self.advance() {
3925                    Token::Star => Ok(Statement::Unlisten(None)),
3926                    Token::Ident(c) | Token::QuotedIdent(c) => Ok(Statement::Unlisten(Some(c))),
3927                    other => Err(self.err(format!(
3928                        "expected channel name or * after UNLISTEN, got {other:?}"
3929                    ))),
3930                }
3931            }
3932            // v7.37.17 (17.6 sibling) — LOCK [TABLE] [ONLY] <table>
3933            // [IN <mode> MODE] [NOWAIT]. SPG's engine holds a
3934            // process-wide write lock today; explicit LOCK has no
3935            // effect. Accept-and-no-op for pg_dump / migration
3936            // compat.
3937            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lock") => {
3938                self.advance();
3939                // v7.39 (round 696) — the LOCK still has no effect (SPG's
3940                // engine holds a process-wide write lock), but the TABLE
3941                // NAME is now carried out so the engine can refuse one that
3942                // does not exist, as PG18 does. MySQL's `LOCK TABLES …
3943                // READ|WRITE` is a different statement with the same first
3944                // word; it keeps the old no-op, because a MySQL dump's
3945                // bracket names tables it is about to create.
3946                let mysql_tables = matches!(self.peek(), Token::Ident(k)
3947                    if k.eq_ignore_ascii_case("tables"));
3948                if mysql_tables {
3949                    self.consume_until_statement_boundary();
3950                    return Ok(Statement::Empty);
3951                }
3952                if matches!(self.peek(), Token::Table) {
3953                    self.advance();
3954                }
3955                let names = self.take_comma_separated_names();
3956                self.consume_until_statement_boundary();
3957                Ok(Statement::ValidateOnly {
3958                    kind: crate::ast::ValidateOnlyKind::LockTable,
3959                    names,
3960                })
3961            }
3962            // v7.37.17 (17.6 sibling) — CHECKPOINT. Forces a WAL
3963            // durability marker + snapshot in PG. SPG has WAL
3964            // checkpointing on a byte / time schedule (v7.37.10
3965            // 60s / 4 MiB defaults). The bare statement parses to
3966            // `Statement::Empty` here (the no_std engine owns no
3967            // WAL / snapshot); v7.37 Epic Du wires the HOST
3968            // (embedded `Database::execute_buffered`, via
3969            // `sql_is_checkpoint`) to force an immediate synchronous
3970            // checkpoint through `Database::checkpoint` — a real
3971            // durability barrier, matching PG.
3972            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("checkpoint") => {
3973                self.advance();
3974                self.consume_until_statement_boundary();
3975                Ok(Statement::Empty)
3976            }
3977            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
3978                self.advance();
3979                self.parse_delete_after_keyword()
3980            }
3981            // v6.0.4: ALTER INDEX <name> REBUILD [WITH (encoding = ...)].
3982            // ALTER is not a reserved keyword in the lexer — handled
3983            // as a bare ident here.
3984            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("alter") => {
3985                self.advance();
3986                self.parse_alter_after_keyword()
3987            }
3988            // v6.1.7: WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>].
3989            // WAIT / POSITION / TIMEOUT are bare idents — no lexer
3990            // additions needed.
3991            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("wait") => {
3992                self.advance();
3993                self.parse_wait_after_keyword()
3994            }
3995            // v6.2.0: ANALYZE [<table>]. ANALYZE is a bare ident.
3996            // Bare ANALYZE → analyse every user table; ANALYZE
3997            // <name> → re-stats one. The argument is an optional
3998            // ident (or quoted ident); anything else is a parse
3999            // error.
4000            // v6.7.3 — `COMPACT COLD SEGMENTS`. No arguments, no
4001            // `WHERE` filter (carved out per V6_7_DESIGN.md
4002            // STABILITY). Lex order: identifier "compact" → "cold"
4003            // → "segments". Anything else after `COMPACT` is a
4004            // parse error.
4005            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("compact") => {
4006                self.advance();
4007                let next = self.peek().clone();
4008                let cold = match next {
4009                    Token::Ident(s) | Token::QuotedIdent(s) => s,
4010                    _ => {
4011                        return Err(
4012                            self.err(format!("expected COLD after COMPACT, got {:?}", self.peek()))
4013                        );
4014                    }
4015                };
4016                if !cold.eq_ignore_ascii_case("cold") {
4017                    return Err(self.err(format!("expected COLD after COMPACT, got {cold:?}")));
4018                }
4019                self.advance();
4020                let next = self.peek().clone();
4021                let segments = match next {
4022                    Token::Ident(s) | Token::QuotedIdent(s) => s,
4023                    _ => {
4024                        return Err(self.err(format!(
4025                            "expected SEGMENTS after COMPACT COLD, got {:?}",
4026                            self.peek()
4027                        )));
4028                    }
4029                };
4030                if !segments.eq_ignore_ascii_case("segments") {
4031                    return Err(self.err(format!(
4032                        "expected SEGMENTS after COMPACT COLD, got {segments:?}"
4033                    )));
4034                }
4035                self.advance();
4036                Ok(Statement::CompactColdSegments)
4037            }
4038            // v7.17.0 Phase 3.P0-42 — SQL:2003 / PG 15+ MERGE.
4039            // Parsed as a case-insensitive identifier since MERGE
4040            // isn't a reserved lexer keyword (collides with the
4041            // mysqldump `ALGORITHM = MERGE` view clause if it
4042            // were); the inner parser drives the rest of the
4043            // surface (USING / ON / WHEN [NOT] MATCHED / THEN).
4044            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge") => {
4045                self.advance();
4046                self.parse_merge_after_keyword()
4047            }
4048            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("analyze") => {
4049                self.advance();
4050                // v7.39.9 — MySQL spells it `ANALYZE TABLE t`. The
4051                // keyword is noise to the parse; what differs is the
4052                // ANSWER, which MySQL returns as a result set — see the
4053                // executor.
4054                let mysql_table_kw = matches!(self.peek(), Token::Table);
4055                if mysql_table_kw {
4056                    self.advance();
4057                }
4058                let target = match self.peek() {
4059                    Token::Eof | Token::Semicolon => None,
4060                    Token::Ident(_) | Token::QuotedIdent(_) => {
4061                        Some(self.expect_ident_like()?)
4062                    }
4063                    other => {
4064                        return Err(self.err(format!(
4065                            "expected table name or end of statement after ANALYZE, got {other:?}"
4066                        )));
4067                    }
4068                };
4069                // v7.39 (round 776, F31 J7) — the per-column form
4070                // (`ANALYZE t (x, y)`, PG-accepted) was a syntax error
4071                // here while the VACUUM arm already consumed it; SPG
4072                // analyzes whole tables, so the list parses and is
4073                // accepted like the VACUUM path's.
4074                if target.is_some() && matches!(self.peek(), Token::LParen) {
4075                    self.advance();
4076                    loop {
4077                        let _ = self.expect_ident_like()?;
4078                        match self.peek() {
4079                            Token::Comma => {
4080                                self.advance();
4081                            }
4082                            Token::RParen => {
4083                                self.advance();
4084                                break;
4085                            }
4086                            other => {
4087                                return Err(self.err(format!(
4088                                    "expected ',' or ')' in ANALYZE column list, got {other:?}"
4089                                )));
4090                            }
4091                        }
4092                    }
4093                }
4094                Ok(Statement::Analyze(target))
4095            }
4096            // v7.12.1 — `SET <name> [TO|=] <value>`. The
4097            // `default_text_search_config` parameter is consumed
4098            // by the FTS function dispatcher; other parameter
4099            // names are recorded but treated as a no-op so PG
4100            // dump output loads.
4101            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {
4102                self.advance();
4103                // PG allows `SET LOCAL` / `SET SESSION` qualifiers; MySQL
4104                // adds `SET GLOBAL` too (and the alias `SET @@global.name =
4105                // …` which the SessionVar path handles). `LOCAL` is the only
4106                // one that changes semantics — it scopes the change to the
4107                // current transaction — so capture it; SESSION / GLOBAL are
4108                // accepted and treated as the default session scope.
4109                let mut set_local = false;
4110                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
4111                    let q = s.to_ascii_lowercase();
4112                    if q == "local" || q == "session" || q == "global" {
4113                        set_local = q == "local";
4114                        self.advance();
4115                    }
4116                }
4117                // 7.38.1 S5.2 — PG `SET [SESSION] AUTHORIZATION
4118                // { DEFAULT | <role> }`. pg_dump's ACL section switches
4119                // to the object owner with it. SPG maps it onto the
4120                // session-role machinery (recorded delta RD-10: PG moves
4121                // session_user too; SPG moves the effective role).
4122                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
4123                    if s.eq_ignore_ascii_case("authorization"))
4124                {
4125                    self.advance(); // AUTHORIZATION
4126                    let role = match self.peek().clone() {
4127                        Token::Default => {
4128                            self.advance();
4129                            None
4130                        }
4131                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4132                            self.advance();
4133                            Some(s)
4134                        }
4135                        _ => None,
4136                    };
4137                    return Ok(Statement::SetRole(role));
4138                }
4139                // v7.14.0 — MySQL `SET NAMES <charset> [COLLATE
4140                // <collation>]` — change the connection client
4141                // charset. SPG stores UTF-8 always and orders
4142                // bytewise; accept as a no-op.
4143                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("names"))
4144                {
4145                    self.advance();
4146                    // v7.39 — this used to parse the clause and throw it
4147                    // away ("SPG stores UTF-8 always and orders
4148                    // bytewise; accept as a no-op"). That sentence
4149                    // stopped being true when collations arrived, and
4150                    // once `collation_connection` began driving literal
4151                    // comparison, dropping the COLLATE clause became a
4152                    // silently wrong answer: `SET NAMES utf8mb4 COLLATE
4153                    // utf8mb4_general_ci` reported back
4154                    // `utf8mb4_0900_ai_ci` and compared as NO PAD.
4155                    //
4156                    // The charset name is emitted as `names` and the
4157                    // ENGINE expands it, because which collation a
4158                    // charset defaults to is MySQL semantics and belongs
4159                    // beside the rest of them, not in the parser.
4160                    let mut pairs = alloc::vec::Vec::new();
4161                    if matches!(
4162                        self.peek(),
4163                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4164                    ) {
4165                        let charset = match self.advance() {
4166                            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4167                            _ => unreachable!("peeked an ident-or-string"),
4168                        };
4169                        pairs.push((String::from("names"), crate::ast::SetValue::Ident(charset)));
4170                    }
4171                    // Optional `COLLATE <name>` — emitted AFTER `names`
4172                    // so it overrides the charset's default, which is
4173                    // what MySQL does.
4174                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate"))
4175                    {
4176                        self.advance();
4177                        if matches!(
4178                            self.peek(),
4179                            Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4180                        ) {
4181                            let coll = match self.advance() {
4182                                Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
4183                                _ => unreachable!("peeked an ident-or-string"),
4184                            };
4185                            pairs.push((
4186                                String::from("collation_connection"),
4187                                crate::ast::SetValue::Ident(coll),
4188                            ));
4189                        }
4190                    }
4191                    if pairs.is_empty() {
4192                        return Ok(Statement::Empty);
4193                    }
4194                    return Ok(Statement::SetParameterList(pairs));
4195                }
4196                // v7.37.17 (17.6 sibling) — PG `SET ROLE
4197                // { NONE | DEFAULT | <role_name> }`. pg_dump preamble
4198                // uses this to switch to the object owner before
4199                // recreating tables. SPG has no role system so this
4200                // is a no-op.
4201                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role"))
4202                {
4203                    self.advance(); // ROLE
4204                    // v7.39 (RLS) — real session-role switch. NONE / DEFAULT
4205                    // reset to the login identity; a name / string sets the
4206                    // effective role that drives current_user + RLS.
4207                    let role = match self.peek().clone() {
4208                        Token::Default => {
4209                            self.advance();
4210                            None
4211                        }
4212                        Token::Ident(s) | Token::QuotedIdent(s)
4213                            if s.eq_ignore_ascii_case("none") =>
4214                        {
4215                            self.advance();
4216                            None
4217                        }
4218                        Token::String(s) | Token::Ident(s) | Token::QuotedIdent(s) => {
4219                            self.advance();
4220                            Some(s)
4221                        }
4222                        _ => None,
4223                    };
4224                    return Ok(Statement::SetRole(role));
4225                }
4226                // v7.37.17 (17.6 sibling) — PG `SET SESSION
4227                // CHARACTERISTICS AS TRANSACTION <mode>` (per PG
4228                // ISO SQL surface). pg_dump prepends this to fix
4229                // the isolation level for the restore session. SPG
4230                // defaults to READ COMMITTED and doesn't yet honor
4231                // session-set isolation across statements — accept
4232                // and no-op. SET (LOCAL/SESSION) TRANSACTION AS ...
4233                // per-tx form is handled elsewhere.
4234                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("characteristics"))
4235                {
4236                    self.advance(); // CHARACTERISTICS
4237                    if matches!(self.peek(), Token::As) {
4238                        self.advance();
4239                    }
4240                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction")) {
4241                        self.advance();
4242                    }
4243                    // v7.39 — no longer a no-op. The note above said SPG
4244                    // "doesn't yet honor session-set isolation across
4245                    // statements"; it does now, through
4246                    // `default_transaction_isolation`, and measured on
4247                    // PG 18.6 this statement is exactly a way to set it:
4248                    //
4249                    //   SET SESSION CHARACTERISTICS AS TRANSACTION
4250                    //       ISOLATION LEVEL REPEATABLE READ;
4251                    //   current_setting('default_transaction_isolation')
4252                    //       -> repeatable read
4253                    //
4254                    // pg_dump prepends this to fix the level for a
4255                    // restore session, so accepting it and doing nothing
4256                    // meant the restore ran at a level nobody chose.
4257                    //
4258                    // The trailing READ ONLY / [NOT] DEFERRABLE modes are
4259                    // still consumed and dropped. `default_transaction_read_only`
4260                    // exists in the GUC inventory but nothing enforces it,
4261                    // and setting a value no code honours is the very
4262                    // defect this version is about — a session told it
4263                    // holds a guarantee it does not.
4264                    let modes = self.parse_isolation_level_clauses()?;
4265                    self.consume_until_statement_boundary();
4266                    let mut pairs: alloc::vec::Vec<(
4267                        alloc::string::String,
4268                        crate::ast::SetValue,
4269                    )> = alloc::vec::Vec::new();
4270                    if let Some(level) = modes.isolation {
4271                        pairs.push((
4272                            alloc::string::String::from("default_transaction_isolation"),
4273                            crate::ast::SetValue::String(alloc::string::String::from(
4274                                level.as_pg_str(),
4275                            )),
4276                        ));
4277                    }
4278                    if let Some(ro) = modes.read_only {
4279                        pairs.push((
4280                            alloc::string::String::from("default_transaction_read_only"),
4281                            crate::ast::SetValue::Ident(alloc::string::String::from(if ro {
4282                                "on"
4283                            } else {
4284                                "off"
4285                            })),
4286                        ));
4287                    }
4288                    return Ok(if pairs.is_empty() {
4289                        Statement::Empty
4290                    } else {
4291                        Statement::SetParameterList(pairs)
4292                    });
4293                }
4294                // v7.37.17 (17.6 sibling) — PG `SET CONSTRAINTS
4295                // { ALL | <name>[, ...] } { DEFERRED | IMMEDIATE }`.
4296                // pg_dump emits this to control the deferrability of
4297                // FK / UNIQUE constraints across a bulk restore. SPG
4298                // has no deferrable-constraint machinery today; the
4299                // FK checker is strict-immediate. Accept-and-no-op
4300                // for pg_dump round-trip compatibility.
4301                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraints"))
4302                {
4303                    self.advance(); // CONSTRAINTS
4304                    // v7.39 (round 288) — no longer a no-op: the trailing
4305                    // DEFERRED / IMMEDIATE sets the transaction's timing.
4306                    // v7.39 (round 308, V29) — and the names are kept.
4307                    // They used to be skipped over on the way to the
4308                    // DEFERRED keyword, so a named form silently behaved
4309                    // as ALL: `SET CONSTRAINTS fk_a DEFERRED` deferred
4310                    // every deferrable constraint in the transaction.
4311                    let mut names: alloc::vec::Vec<alloc::string::String> =
4312                        alloc::vec::Vec::new();
4313                    if matches!(self.peek(), Token::All) {
4314                        self.advance();
4315                    } else {
4316                        loop {
4317                            let mut n = self.expect_ident_like()?;
4318                            // A schema-qualified name (`public.fk_a`)
4319                            // identifies the same constraint; PG resolves
4320                            // it by the trailing segment.
4321                            while matches!(self.peek(), Token::Dot) {
4322                                self.advance();
4323                                n = self.expect_ident_like()?;
4324                            }
4325                            names.push(n);
4326                            if matches!(self.peek(), Token::Comma) {
4327                                self.advance();
4328                            } else {
4329                                break;
4330                            }
4331                        }
4332                    }
4333                    let deferred = match self.peek() {
4334                        Token::Ident(s) | Token::QuotedIdent(s)
4335                            if s.eq_ignore_ascii_case("deferred") =>
4336                        {
4337                            true
4338                        }
4339                        Token::Ident(s) | Token::QuotedIdent(s)
4340                            if s.eq_ignore_ascii_case("immediate") =>
4341                        {
4342                            false
4343                        }
4344                        other => {
4345                            return Err(self.err(alloc::format!(
4346                                "expected DEFERRED or IMMEDIATE after SET CONSTRAINTS, got {other:?}"
4347                            )));
4348                        }
4349                    };
4350                    self.advance();
4351                    return Ok(Statement::SetConstraints { names, deferred });
4352                }
4353                // v7.16.2 — PG `SET [SESSION] AUTHORIZATION
4354                // { DEFAULT | '<role>' | <ident> }` (mailrs
4355                // round-10 A.1). pg_dump preamble emits the
4356                // `DEFAULT` form to reset session authorization.
4357                //
4358                // v7.39 (round 697) — this said "SPG has no role system so
4359                // this is a strict no-op". SPG has had one since round 58;
4360                // the comment outlived it, and with it the reason a name
4361                // that is not a role was accepted here. It still switches
4362                // no authorization — what it does now is refuse a role
4363                // that does not exist, as PG18 does. PG also accepts `RESET SESSION
4364                // AUTHORIZATION` (handled by the RESET parser
4365                // elsewhere). Reference:
4366                // <https://www.postgresql.org/docs/current/sql-set-session-authorization.html>
4367                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("authorization"))
4368                {
4369                    self.advance(); // AUTHORIZATION
4370                    match self.peek().clone() {
4371                        Token::Default => {
4372                            self.advance();
4373                        }
4374                        Token::String(r) | Token::Ident(r) | Token::QuotedIdent(r) => {
4375                            self.advance();
4376                            return Ok(Statement::ValidateOnly {
4377                                kind: crate::ast::ValidateOnlyKind::RoleName,
4378                                names: alloc::vec![r],
4379                            });
4380                        }
4381                        other => {
4382                            return Err(self.err(alloc::format!(
4383                                "expected DEFAULT / '<role>' / <ident> after SET SESSION AUTHORIZATION, got {other:?}"
4384                            )));
4385                        }
4386                    }
4387                    return Ok(Statement::Empty);
4388                }
4389                // v7.38 轴 4 — `SET [SESSION] TRANSACTION
4390                // ISOLATION LEVEL { READ COMMITTED | READ
4391                // UNCOMMITTED | REPEATABLE READ | SERIALIZABLE }
4392                // [, READ {ONLY|WRITE}] [, [NOT] DEFERRABLE]`.
4393                // PG-standard surface. v7.37.8 accepts the syntax
4394                // and tracks the selected level on
4395                // `Engine::current_isolation_level()`; the actual
4396                // MVCC / SSI semantics implementation lands in
4397                // the 轴 4 isolation framework (separate train).
4398                // PG itself maps READ UNCOMMITTED to READ COMMITTED
4399                // internally; SPG behaves the same (effectively
4400                // READ COMMITTED at every level today).
4401                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("transaction"))
4402                {
4403                    self.advance(); // TRANSACTION
4404                    let modes = self.parse_isolation_level_clauses()?;
4405                    return Ok(Statement::SetTransaction { modes });
4406                }
4407                // v7.14.0 — MySQL `SET CHARACTER SET <charset>`
4408                // alias — same accept-as-no-op as SET NAMES.
4409                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
4410                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
4411                {
4412                    self.advance(); // CHARACTER
4413                    self.advance(); // SET
4414                    if matches!(
4415                        self.peek(),
4416                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
4417                    ) {
4418                        self.advance();
4419                    }
4420                    return Ok(Statement::Empty);
4421                }
4422                // v7.39 (GUC) — PG spells the timezone GUC as two
4423                // keywords: `SET [LOCAL|SESSION] TIME ZONE <value>`,
4424                // where <value> is a string/ident or the LOCAL /
4425                // DEFAULT keyword (both mean "back to the default").
4426                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("time"))
4427                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
4428                {
4429                    self.advance(); // TIME
4430                    self.advance(); // ZONE
4431                    let value = match self.peek().clone() {
4432                        Token::Ident(s)
4433                            if s.eq_ignore_ascii_case("local")
4434                                || s.eq_ignore_ascii_case("default") =>
4435                        {
4436                            self.advance();
4437                            crate::ast::SetValue::Default
4438                        }
4439                        Token::Default => {
4440                            self.advance();
4441                            crate::ast::SetValue::Default
4442                        }
4443                        _ => self.parse_set_value()?,
4444                    };
4445                    return Ok(Statement::SetParameter {
4446                        name: "timezone".into(),
4447                        value,
4448                        local: set_local,
4449                    });
4450                }
4451                // v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]` is a
4452                // MySQL USER-variable assignment: its own per-session
4453                // namespace, an arbitrary expression on the right, and `:=`
4454                // as a second spelling of `=`. It used to fall into the
4455                // session-PARAMETER list below, whose values are literals and
4456                // whose store nothing reads back under a `@` name — so the
4457                // assignment reported success and vanished.
4458                //
4459                // A `@@`-prefixed LHS is a real engine setting and keeps the
4460                // old path.
4461                if matches!(self.peek(), Token::SessionVar(s) if !s.starts_with("@@")) {
4462                    return self.parse_set_user_vars();
4463                }
4464                // v7.14.0 — multi-assignment form
4465                // `SET a = 1, b = 2, …`. Single-assignment is the
4466                // 1-element case. Each LHS may be a regular ident
4467                // or a SessionVar (`@VAR` / `@@VAR`).
4468                let mut pairs: Vec<(String, crate::ast::SetValue)> = Vec::new();
4469                loop {
4470                    let lhs = match self.peek().clone() {
4471                        Token::SessionVar(s) => {
4472                            self.advance();
4473                            s
4474                        }
4475                        Token::Ident(_) | Token::QuotedIdent(_) => self.parse_set_param_name()?,
4476                        other => {
4477                            return Err(self.err(format!(
4478                                "expected parameter name after SET, got {other:?}"
4479                            )));
4480                        }
4481                    };
4482                    // Accept either `=` or the bare `TO` keyword.
4483                    match self.peek() {
4484                        Token::Eq => {
4485                            self.advance();
4486                        }
4487                        Token::To => {
4488                            self.advance();
4489                        }
4490                        other => {
4491                            return Err(self.err(format!(
4492                                "expected `=` or TO after SET {lhs}, got {other:?}"
4493                            )));
4494                        }
4495                    }
4496                    let mut value = self.parse_set_value()?;
4497                    // v7.39 (GUC) — disambiguate the comma: `, name =` /
4498                    // `, name TO` continues a MySQL-style multi-assign,
4499                    // anything else is a PG list VALUE
4500                    // (`SET search_path = myschema, public`) folded into
4501                    // one comma-joined string.
4502                    while matches!(self.peek(), Token::Comma) {
4503                        let is_assign = matches!(
4504                            self.tokens.get(self.pos + 1),
4505                            Some(Token::Ident(_) | Token::QuotedIdent(_) | Token::SessionVar(_))
4506                        ) && matches!(
4507                            self.tokens.get(self.pos + 2),
4508                            Some(Token::Eq | Token::To)
4509                        );
4510                        if is_assign {
4511                            break;
4512                        }
4513                        self.advance(); // comma
4514                        let next = self.parse_set_value()?;
4515                        let joined = alloc::format!(
4516                            "{}, {}",
4517                            set_value_text(&value),
4518                            set_value_text(&next)
4519                        );
4520                        value = crate::ast::SetValue::String(joined);
4521                    }
4522                    pairs.push((lhs, value));
4523                    if matches!(self.peek(), Token::Comma) {
4524                        self.advance();
4525                        continue;
4526                    }
4527                    break;
4528                }
4529                if pairs.len() == 1 {
4530                    let (name, value) = pairs.into_iter().next().unwrap();
4531                    Ok(Statement::SetParameter {
4532                        name,
4533                        value,
4534                        local: set_local,
4535                    })
4536                } else {
4537                    Ok(Statement::SetParameterList(pairs))
4538                }
4539            }
4540            // v7.12.1 — `RESET <name>` / `RESET ALL`.
4541            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reset") => {
4542                self.advance();
4543                match self.peek().clone() {
4544                    Token::All => {
4545                        self.advance();
4546                        Ok(Statement::ResetParameter(None))
4547                    }
4548                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("all") => {
4549                        self.advance();
4550                        Ok(Statement::ResetParameter(None))
4551                    }
4552                    // v7.39 (RLS) — `RESET ROLE` clears the session role.
4553                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4554                        self.advance();
4555                        Ok(Statement::SetRole(None))
4556                    }
4557                    // 7.38.1 S5.2 — `RESET SESSION AUTHORIZATION`
4558                    // (pg_dump's return from the owner switch).
4559                    Token::Ident(s) | Token::QuotedIdent(s)
4560                        if s.eq_ignore_ascii_case("session")
4561                            && matches!(
4562                                self.tokens.get(self.pos + 1),
4563                                Some(Token::Ident(a) | Token::QuotedIdent(a))
4564                                    if a.eq_ignore_ascii_case("authorization")
4565                            ) =>
4566                    {
4567                        self.advance(); // SESSION
4568                        self.advance(); // AUTHORIZATION
4569                        Ok(Statement::SetRole(None))
4570                    }
4571                    _ => {
4572                        let name = self.parse_set_param_name()?;
4573                        Ok(Statement::ResetParameter(Some(name)))
4574                    }
4575                }
4576            }
4577            // v7.39 (round 218) — server-side cursors.
4578            Token::Ident(s) if s.eq_ignore_ascii_case("declare") => self.parse_declare_cursor(),
4579            Token::Ident(s) if s.eq_ignore_ascii_case("fetch") => self.parse_fetch_or_move(false),
4580            Token::Ident(s) if s.eq_ignore_ascii_case("move") => self.parse_fetch_or_move(true),
4581            Token::Ident(s) if s.eq_ignore_ascii_case("close") => {
4582                self.advance();
4583                match self.peek().clone() {
4584                    Token::All => {
4585                        self.advance();
4586                        Ok(Statement::CloseCursor { name: None })
4587                    }
4588                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4589                        self.advance();
4590                        Ok(Statement::CloseCursor { name: None })
4591                    }
4592                    Token::Ident(n) | Token::QuotedIdent(n) => {
4593                        self.advance();
4594                        Ok(Statement::CloseCursor { name: Some(n) })
4595                    }
4596                    other => Err(self.err(format!(
4597                        "expected cursor name or ALL after CLOSE, got {other:?}"
4598                    ))),
4599                }
4600            }
4601            other => Err(self.err(format!(
4602                "expected SELECT / CREATE / DROP / INSERT / UPDATE / DELETE / ALTER / BEGIN / COMMIT / \
4603                 ROLLBACK / SAVEPOINT / RELEASE / SHOW at start of statement, got {other:?}"
4604            ))),
4605        }
4606    }
4607
4608    /// v7.39 (round 218) — `DECLARE <name> [BINARY] [INSENSITIVE] [ASENSITIVE]
4609    /// [[NO] SCROLL] CURSOR [{WITH|WITHOUT} HOLD] FOR <select>`. BINARY /
4610    /// (IN|A)SENSITIVE are accepted and ignored (SPG cursors materialize at
4611    /// DECLARE, which is INSENSITIVE — PG's only actual behaviour too).
4612    fn parse_declare_cursor(&mut self) -> Result<Statement, ParseError> {
4613        self.advance(); // DECLARE
4614        let name = match self.advance() {
4615            Token::Ident(n) | Token::QuotedIdent(n) => n,
4616            other => {
4617                return Err(self.err(format!("expected cursor name after DECLARE, got {other:?}")));
4618            }
4619        };
4620        let mut scroll: Option<bool> = None;
4621        loop {
4622            match self.peek() {
4623                Token::Ident(s)
4624                    if s.eq_ignore_ascii_case("binary")
4625                        || s.eq_ignore_ascii_case("insensitive")
4626                        || s.eq_ignore_ascii_case("asensitive") =>
4627                {
4628                    self.advance();
4629                }
4630                Token::Ident(s) if s.eq_ignore_ascii_case("scroll") => {
4631                    self.advance();
4632                    scroll = Some(true);
4633                }
4634                Token::Not | Token::Ident(_) if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("no")) =>
4635                {
4636                    self.advance(); // NO
4637                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("scroll")) {
4638                        return Err(self.err(format!(
4639                            "expected SCROLL after NO in DECLARE, got {:?}",
4640                            self.peek()
4641                        )));
4642                    }
4643                    self.advance();
4644                    scroll = Some(false);
4645                }
4646                _ => break,
4647            }
4648        }
4649        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cursor")) {
4650            return Err(self.err(format!("expected CURSOR in DECLARE, got {:?}", self.peek())));
4651        }
4652        self.advance();
4653        let mut hold = false;
4654        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
4655            self.advance();
4656            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4657                return Err(self.err(format!(
4658                    "expected HOLD after WITH in DECLARE, got {:?}",
4659                    self.peek()
4660                )));
4661            }
4662            self.advance();
4663            hold = true;
4664        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without")) {
4665            self.advance();
4666            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("hold")) {
4667                return Err(self.err(format!(
4668                    "expected HOLD after WITHOUT in DECLARE, got {:?}",
4669                    self.peek()
4670                )));
4671            }
4672            self.advance();
4673        }
4674        if !matches!(self.peek(), Token::For) {
4675            return Err(self.err(format!(
4676                "expected FOR before the cursor query, got {:?}",
4677                self.peek()
4678            )));
4679        }
4680        self.advance();
4681        let query = self.parse_one_statement()?;
4682        Ok(Statement::DeclareCursor {
4683            name,
4684            scroll,
4685            hold,
4686            query: alloc::boxed::Box::new(query),
4687        })
4688    }
4689
4690    /// v7.39 (round 218) — `FETCH`/`MOVE` `[<direction>] [FROM|IN] <name>`.
4691    /// Direction: NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n |
4692    /// ALL | FORWARD [n|ALL] | BACKWARD [n|ALL]; bare `FETCH <name>` = NEXT.
4693    fn parse_fetch_or_move(&mut self, is_move: bool) -> Result<Statement, ParseError> {
4694        use crate::ast::CursorDirection as D;
4695        self.advance(); // FETCH / MOVE
4696        let mut signed_count = |this: &mut Self| -> Result<i64, ParseError> {
4697            let neg = if matches!(this.peek(), Token::Minus) {
4698                this.advance();
4699                true
4700            } else {
4701                false
4702            };
4703            match this.advance() {
4704                Token::Integer(v) => Ok(if neg { -v } else { v }),
4705                other => Err(this.err(format!("expected count, got {other:?}"))),
4706            }
4707        };
4708        let direction = match self.peek().clone() {
4709            Token::Ident(s) if s.eq_ignore_ascii_case("next") => {
4710                self.advance();
4711                D::Next
4712            }
4713            Token::Ident(s) if s.eq_ignore_ascii_case("prior") => {
4714                self.advance();
4715                D::Prior
4716            }
4717            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
4718                self.advance();
4719                D::First
4720            }
4721            Token::Ident(s) if s.eq_ignore_ascii_case("last") => {
4722                self.advance();
4723                D::Last
4724            }
4725            Token::Ident(s) if s.eq_ignore_ascii_case("absolute") => {
4726                self.advance();
4727                D::Absolute(signed_count(self)?)
4728            }
4729            Token::Ident(s) if s.eq_ignore_ascii_case("relative") => {
4730                self.advance();
4731                D::Relative(signed_count(self)?)
4732            }
4733            Token::Ident(s) if s.eq_ignore_ascii_case("forward") => {
4734                self.advance();
4735                match self.peek().clone() {
4736                    Token::All => {
4737                        self.advance();
4738                        D::All
4739                    }
4740                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4741                        self.advance();
4742                        D::All
4743                    }
4744                    Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4745                    _ => D::Next, // bare FORWARD = FORWARD 1
4746                }
4747            }
4748            Token::Ident(s) if s.eq_ignore_ascii_case("backward") => {
4749                self.advance();
4750                match self.peek().clone() {
4751                    Token::All => {
4752                        self.advance();
4753                        D::BackwardAll
4754                    }
4755                    Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4756                        self.advance();
4757                        D::BackwardAll
4758                    }
4759                    Token::Integer(_) | Token::Minus => D::Backward(signed_count(self)?),
4760                    _ => D::Backward(1), // bare BACKWARD = BACKWARD 1
4761                }
4762            }
4763            Token::All => {
4764                self.advance();
4765                D::All
4766            }
4767            Token::Ident(s) if s.eq_ignore_ascii_case("all") => {
4768                self.advance();
4769                D::All
4770            }
4771            Token::Integer(_) | Token::Minus => D::Count(signed_count(self)?),
4772            // Bare `FETCH <name>` — direction defaults to NEXT.
4773            _ => D::Next,
4774        };
4775        // Optional FROM / IN.
4776        if matches!(self.peek(), Token::From)
4777            || matches!(self.peek(), Token::In)
4778            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("in"))
4779        {
4780            self.advance();
4781        }
4782        let name = match self.advance() {
4783            Token::Ident(n) | Token::QuotedIdent(n) => n,
4784            other => {
4785                return Err(self.err(format!("expected cursor name, got {other:?}")));
4786            }
4787        };
4788        Ok(if is_move {
4789            Statement::MoveCursor { name, direction }
4790        } else {
4791            Statement::FetchCursor { name, direction }
4792        })
4793    }
4794
4795    /// v7.39 (round 280) — `CREATE STATISTICS [IF NOT EXISTS] <name>
4796    /// [(kind, …)] ON <col>, … FROM <table>`.
4797    fn parse_create_statistics_after_create(&mut self) -> Result<Statement, ParseError> {
4798        self.advance(); // STATISTICS
4799        // `IF` / `EXISTS` lex as plain identifiers; only NOT is a keyword.
4800        let mut if_not_exists = false;
4801        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4802            && matches!(self.tokens.get(self.pos + 1), Some(Token::Not))
4803        {
4804            self.advance();
4805            self.advance();
4806            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
4807                self.advance();
4808                if_not_exists = true;
4809            }
4810        }
4811        let name = self.expect_ident_like()?;
4812        let mut kinds = Vec::new();
4813        if matches!(self.peek(), Token::LParen) {
4814            self.advance();
4815            loop {
4816                let k = self.expect_ident_like()?;
4817                // PG stores the single letters; accept the spelled-out
4818                // names the SQL uses and record what PG records.
4819                kinds.push(match k.to_ascii_lowercase().as_str() {
4820                    "ndistinct" => String::from("d"),
4821                    "dependencies" => String::from("f"),
4822                    "mcv" => String::from("m"),
4823                    other => {
4824                        return Err(
4825                            self.err(alloc::format!("unrecognized statistics kind \"{other}\""))
4826                        );
4827                    }
4828                });
4829                match self.advance() {
4830                    Token::Comma => {}
4831                    Token::RParen => break,
4832                    other => {
4833                        return Err(self.err(alloc::format!(
4834                            "expected ',' or ')' in statistics kind list, got {other:?}"
4835                        )));
4836                    }
4837                }
4838            }
4839        }
4840        if !matches!(self.peek(), Token::On) {
4841            return Err(self.err(alloc::format!(
4842                "expected ON in CREATE STATISTICS, got {:?}",
4843                self.peek()
4844            )));
4845        }
4846        self.advance();
4847        let mut columns = Vec::new();
4848        loop {
4849            columns.push(self.expect_ident_like()?);
4850            if matches!(self.peek(), Token::Comma) {
4851                self.advance();
4852            } else {
4853                break;
4854            }
4855        }
4856        if !matches!(self.peek(), Token::From) {
4857            return Err(self.err(alloc::format!(
4858                "expected FROM in CREATE STATISTICS, got {:?}",
4859                self.peek()
4860            )));
4861        }
4862        self.advance();
4863        let table = self.expect_ident_like()?;
4864        Ok(Statement::CreateStatistics {
4865            name,
4866            if_not_exists,
4867            kinds,
4868            columns,
4869            table,
4870        })
4871    }
4872
4873    /// v7.39 (round 280) — `DROP STATISTICS [IF EXISTS] <name>`.
4874    /// v7.39 (round 436) — the body of `DROP TABLE [IF EXISTS] a[, b] …`,
4875    /// entered with the `TABLE` keyword still unconsumed. Extracted so
4876    /// `DROP TEMPORARY TABLE` (MySQL) runs the identical grammar instead of
4877    /// a second copy — the parser cannot rewind, so re-dispatch has to be a
4878    /// forward call.
4879    fn parse_drop_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
4880        self.advance(); // TABLE
4881        let if_exists = self.consume_if_exists();
4882        let mut names: Vec<String> = Vec::new();
4883        loop {
4884            names.push(self.expect_ident_like()?);
4885            if matches!(self.peek(), Token::Comma) {
4886                self.advance();
4887                continue;
4888            }
4889            break;
4890        }
4891        if matches!(
4892            self.peek(),
4893            Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
4894                || s.eq_ignore_ascii_case("restrict")
4895        ) {
4896            self.advance();
4897        }
4898        Ok(Statement::DropTable { names, if_exists })
4899    }
4900
4901    fn parse_drop_statistics_after_drop(&mut self) -> Result<Statement, ParseError> {
4902        self.advance(); // STATISTICS
4903        let mut if_exists = false;
4904        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
4905            && matches!(self.tokens.get(self.pos + 1),
4906                        Some(Token::Ident(e)) if e.eq_ignore_ascii_case("exists"))
4907        {
4908            self.advance();
4909            self.advance();
4910            if_exists = true;
4911        }
4912        let name = self.expect_ident_like()?;
4913        Ok(Statement::DropStatistics { name, if_exists })
4914    }
4915
4916    fn parse_create_stmt(&mut self) -> Result<Statement, ParseError> {
4917        debug_assert!(matches!(self.peek(), Token::Create));
4918        self.advance();
4919        match self.peek() {
4920            Token::Table => self.parse_create_table_stmt_after_create(),
4921            Token::Index => self.parse_create_index_stmt_after_create(false),
4922            // v7.39 (round 280) — CREATE STATISTICS is a real catalog
4923            // object now. It used to be consumed by the CREATE-noise
4924            // arm, so a pg_dump that declares extended statistics
4925            // restored silently without them and reflection showed
4926            // nothing.
4927            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("statistics") => {
4928                self.parse_create_statistics_after_create()
4929            }
4930            // v7.9.29 — `CREATE UNIQUE INDEX … [WHERE pred]`.
4931            // The `UNIQUE` modifier turns a partial index into a
4932            // partial-uniqueness invariant (only rows matching the
4933            // WHERE predicate are checked for duplicates). mailrs
4934            // K1 (3 hits: email_templates default, calendar_events
4935            // master, calendar_events instance).
4936            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unique") => {
4937                self.advance();
4938                if !matches!(self.peek(), Token::Index) {
4939                    return Err(self.err(alloc::format!(
4940                        "expected INDEX after CREATE UNIQUE, got {:?}",
4941                        self.peek()
4942                    )));
4943                }
4944                self.parse_create_index_stmt_after_create(true)
4945            }
4946            Token::Publication => {
4947                self.advance();
4948                self.parse_create_publication_after_keyword()
4949            }
4950            Token::Subscription => {
4951                self.advance();
4952                self.parse_create_subscription_after_keyword()
4953            }
4954            // v4.1: CREATE USER 'name' WITH PASSWORD 'pw' [ROLE 'role'].
4955            // USER isn't a reserved keyword — we look for the bare
4956            // identifier so the lexer doesn't have to grow a token.
4957            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("user") => {
4958                self.advance();
4959                self.parse_create_user_after_keyword(true)
4960            }
4961            // v7.39 (read01 round 58) — `CREATE ROLE name [WITH] [options]`.
4962            // PG's CREATE USER *is* CREATE ROLE … LOGIN; the only difference is
4963            // the default of the LOGIN attribute.
4964            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("role") => {
4965                self.advance();
4966                self.parse_create_user_after_keyword(false)
4967            }
4968            // v7.39 (RLS) — `CREATE POLICY name ON table …`.
4969            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
4970                self.advance();
4971                self.parse_create_policy_after_keyword()
4972            }
4973            // v7.9.15 — `CREATE EXTENSION [IF NOT EXISTS] <name>
4974            // [WITH SCHEMA …] [VERSION '…'] [CASCADE]` as a
4975            // no-op. mailrs follow-up F3.
4976            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("extension") => {
4977                self.advance();
4978                self.parse_create_extension_after_keyword()
4979            }
4980            // v7.12.4 — `CREATE [OR REPLACE] FUNCTION …` and
4981            // `CREATE [OR REPLACE] TRIGGER …`. `OR REPLACE` is
4982            // optional; absorb it here and forward to the
4983            // per-kind parsers with the flag. OR is a reserved
4984            // keyword token.
4985            Token::Or => {
4986                self.advance();
4987                let next = self.peek();
4988                let (Token::Ident(s2) | Token::QuotedIdent(s2)) = next else {
4989                    return Err(self.err(alloc::format!(
4990                        "expected REPLACE after CREATE OR, got {next:?}"
4991                    )));
4992                };
4993                if !s2.eq_ignore_ascii_case("replace") {
4994                    return Err(self.err(alloc::format!(
4995                        "expected REPLACE after CREATE OR, got {s2:?}"
4996                    )));
4997                }
4998                self.advance();
4999                self.parse_create_function_or_trigger_after_or_replace(true)
5000            }
5001            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("function") => {
5002                self.advance();
5003                self.parse_create_function_after_keyword(false)
5004            }
5005            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("trigger") => {
5006                self.advance();
5007                self.parse_create_trigger_after_keyword(false)
5008            }
5009            // v7.39 (round 139) — CREATE RULE …
5010            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rule") => {
5011                self.advance();
5012                self.parse_create_rule_after_keyword(false)
5013            }
5014            // v7.39 (read01 round 82) — CREATE CONSTRAINT TRIGGER. A constraint
5015            // trigger is a row-level AFTER trigger that additionally carries
5016            // DEFERRABLE / INITIALLY DEFERRED timing; the `parse_create_trigger`
5017            // path already tolerates and skips those clauses, so consuming the
5018            // CONSTRAINT keyword and reusing it makes the statement parse and the
5019            // trigger fire. (The deferral timing itself is not yet honoured —
5020            // SPG fires it as a plain AFTER trigger, which is correct behaviour
5021            // for every non-deferred use.)
5022            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5023                self.advance();
5024                if !matches!(self.peek(), Token::Ident(t) | Token::QuotedIdent(t)
5025                    if t.eq_ignore_ascii_case("trigger"))
5026                {
5027                    return Err(self.err(alloc::format!(
5028                        "expected TRIGGER after CREATE CONSTRAINT, got {:?}",
5029                        self.peek()
5030                    )));
5031                }
5032                self.advance();
5033                self.parse_create_trigger_after_keyword(false)
5034            }
5035            // v7.17.0 — CREATE [TEMPORARY] SEQUENCE …
5036            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
5037                self.advance();
5038                self.parse_create_sequence_after_keyword(false)
5039            }
5040            // v7.17.0 Phase 1.2 — CREATE [TEMPORARY] VIEW …
5041            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("view") => {
5042                self.advance();
5043                self.parse_create_view_after_keyword(false, false, false)
5044            }
5045            // v7.17.0 Phase 2.6 — MySQL view prefix clauses
5046            // `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}` /
5047            // `DEFINER = <user>` / `SQL SECURITY {DEFINER|INVOKER}`
5048            // appear (in any order) between `CREATE` and `VIEW` in
5049            // every mysqldump-emitted view. Pre-2.6 the parser
5050            // rejected the prefix and the customer's whole view
5051            // backup failed on the first view. The hints are pure
5052            // planner / permission metadata; SPG's view-rewrite
5053            // path is semantically equivalent for all three
5054            // algorithms in v7.17 (TEMPTABLE differs only in
5055            // perf for huge views — out of v7.17 scope), and
5056            // DEFINER / SQL SECURITY are pure single-user
5057            // permissioning that SPG ignores by design.
5058            Token::Ident(s) | Token::QuotedIdent(s)
5059                if s.eq_ignore_ascii_case("algorithm")
5060                    || s.eq_ignore_ascii_case("definer")
5061                    || s.eq_ignore_ascii_case("sql") =>
5062            {
5063                self.consume_mysql_view_prefix()?;
5064                // After absorbing ALGORITHM / DEFINER / SQL SECURITY
5065                // (in any order, in any combination), the next
5066                // keyword must be VIEW. mysqldump never emits these
5067                // prefixes on non-view statements.
5068                let next = self.peek().clone();
5069                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2)
5070                    if s2.eq_ignore_ascii_case("view"))
5071                {
5072                    self.advance();
5073                    self.parse_create_view_after_keyword(false, false, false)
5074                } else {
5075                    Err(self.err(alloc::format!(
5076                        "expected VIEW after MySQL view prefix (ALGORITHM/DEFINER/SQL SECURITY), got {next:?}"
5077                    )))
5078                }
5079            }
5080            // v7.17.0 Phase 1.4 — CREATE TYPE name AS ENUM (…).
5081            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
5082                self.advance();
5083                self.parse_create_type_after_keyword()
5084            }
5085            // v7.17.0 Phase 1.5 — CREATE DOMAIN name AS base
5086            // [DEFAULT expr] [NOT NULL] [CHECK (expr)]*.
5087            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
5088                self.advance();
5089                self.parse_create_domain_after_keyword()
5090            }
5091            // v7.17.0 Phase 1.6 — CREATE SCHEMA [IF NOT EXISTS]
5092            // name [AUTHORIZATION user]. Real catalog registry
5093            // (was silent-no-op'd pre-v7.17).
5094            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("schema") => {
5095                self.advance();
5096                let if_not_exists = self.parse_if_not_exists();
5097                let name = self.expect_ident_like()?;
5098                // Optional `AUTHORIZATION <user>` trailer — accepted,
5099                // ignored (single-user catalog).
5100                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
5101                    if s.eq_ignore_ascii_case("authorization"))
5102                {
5103                    self.advance();
5104                    let _ = self.expect_ident_like()?;
5105                }
5106                Ok(Statement::CreateSchema { name, if_not_exists })
5107            }
5108            // v7.17.0 Phase 1.3 — CREATE MATERIALIZED VIEW …
5109            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("materialized") => {
5110                self.advance();
5111                let next = self.peek().clone();
5112                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5113                {
5114                    self.advance();
5115                    self.parse_create_materialized_view_after_keyword()
5116                } else {
5117                    Err(self.err(alloc::format!(
5118                        "expected VIEW after CREATE MATERIALIZED, got {next:?}"
5119                    )))
5120                }
5121            }
5122            // v7.38 (read01 P6.57) — CREATE UNLOGGED TABLE. Unlike TEMP (a
5123            // no-op below), an UNLOGGED table is a real, fully-usable table in
5124            // PG — it only skips WAL. SPG creates a normal table (the WAL-skip
5125            // durability optimisation is a follow-up), so a dump / app that
5126            // declares UNLOGGED tables works instead of failing to parse.
5127            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unlogged") => {
5128                self.advance(); // UNLOGGED
5129                if matches!(self.peek(), Token::Table) {
5130                    self.parse_create_table_stmt_after_create()
5131                } else {
5132                    Err(self.err(format!(
5133                        "expected TABLE after CREATE UNLOGGED, got {:?}",
5134                        self.peek()
5135                    )))
5136                }
5137            }
5138            Token::Ident(s) | Token::QuotedIdent(s)
5139                if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") =>
5140            {
5141                self.advance();
5142                // TEMPORARY/TEMP followed by SEQUENCE / VIEW.
5143                let next = self.peek().clone();
5144                if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("sequence"))
5145                {
5146                    self.advance();
5147                    self.parse_create_sequence_after_keyword(true)
5148                } else if matches!(&next, Token::Ident(s2) | Token::QuotedIdent(s2) if s2.eq_ignore_ascii_case("view"))
5149                {
5150                    self.advance();
5151                    self.parse_create_view_after_keyword(false, false, true)
5152                } else {
5153                    // v7.39 (round 436) — `CREATE TEMPORARY TABLE` used to be
5154                    // consumed and answered OK while creating nothing, so
5155                    // every statement that touched the table afterwards failed
5156                    // with "table not found" — the DDL itself lied. It is a
5157                    // real CREATE TABLE now, marked temporary so the executor
5158                    // puts it in the session's own namespace. An optional
5159                    // TABLE keyword may or may not be present (`CREATE TEMP t`
5160                    // is not legal, but the keyword is consumed by the
5161                    // CREATE TABLE parser itself).
5162                    let stmt = self.parse_create_table_stmt_after_create()?;
5163                    match stmt {
5164                        Statement::CreateTable(mut c) => {
5165                            c.temporary = true;
5166                            Ok(Statement::CreateTable(c))
5167                        }
5168                        // `CREATE TEMPORARY TABLE x AS <select>` lowers to the
5169                        // CTAS node, which needs the same session namespace.
5170                        Statement::CreateMaterializedView(mut m) if m.as_plain_table => {
5171                            m.temporary = true;
5172                            Ok(Statement::CreateMaterializedView(m))
5173                        }
5174                        other => Ok(other),
5175                    }
5176                }
5177            }
5178            // v7.17.0 Phase 4.2 — MySQL `CREATE PROCEDURE name (…)
5179            // BEGIN <body> END`. The body may reference `@var`
5180            // session variables, SET statements, internal `;`
5181            // terminators, etc. SPG has no procedure runtime, so
5182            // consume the whole `CREATE PROCEDURE … END` block as
5183            // a no-op so mysqldump scripts that include stored
5184            // routines load through. The matching-END consumer
5185            // tracks BEGIN/END nesting depth to handle nested
5186            // BEGIN blocks correctly.
5187            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("procedure") => {
5188                self.consume_mysql_routine_body();
5189                Ok(Statement::Empty)
5190            }
5191            // v7.14.0 — pg_dump / mysqldump emit
5192            // `CREATE SCHEMA / VIEW / MATERIALIZED VIEW /
5193            // TYPE / DOMAIN / DATABASE / ROLE / POLICY / OPERATOR`.
5194            // SPG is single-schema / single-database; these have
5195            // no behavioural effect, so consume + return Empty.
5196            // v7.17.0 NOTE: SEQUENCE / VIEW / MATERIALIZED VIEW /
5197            // TYPE / DOMAIN / SCHEMA were here pre-v7.17; all
5198            // moved up to real parser branches. DATABASE / ROLE /
5199            // POLICY / OPERATOR stay no-op forever
5200            // (single-database, hardcoded roles).
5201            Token::Ident(s) | Token::QuotedIdent(s)
5202                if matches!(
5203                    s.to_ascii_lowercase().as_str(),
5204                    "database"
5205                        | "role"
5206                        | "operator"
5207                        | "cast"
5208                        | "aggregate"
5209                        | "language"
5210                        | "collation"
5211                        | "conversion"
5212                        // v7.17.0 Phase 8 (audit N6) — rarely-
5213                        // emitted pg_dump shapes that should
5214                        // load through without a parser error.
5215                        // SPG has no planner statistics catalog,
5216                        // no event-trigger hooks, no foreign-
5217                        // data-wrapper infrastructure; consume
5218                        // + return Empty.
5219                        | "statistics"
5220                        | "event"
5221                        // v7.37.17 (17.6 siblings) — additional CREATE
5222                        // targets pg_dump / operator install scripts
5223                        // may emit that SPG has no matching machinery
5224                        // for. Consume + Empty-return.
5225                        | "text"
5226                        | "tablespace"
5227                        | "access"
5228                        | "large"
5229                ) =>
5230            {
5231                // DATABASE is the one member of this list PG refuses
5232                // inside a transaction block; the rest (ROLE, CAST,
5233                // TABLESPACE, …) it runs there quite happily, so only
5234                // this one is named. Still a no-op otherwise — SPG is
5235                // single-database.
5236                let is_database = s.eq_ignore_ascii_case("database");
5237                // The name is the first token after DATABASE, past an
5238                // `IF NOT EXISTS`.
5239                let name = if is_database {
5240                    self.scan_database_name()
5241                } else {
5242                    None
5243                };
5244                let collation = if is_database {
5245                    self.scan_database_collation_until_boundary()
5246                } else {
5247                    self.consume_until_statement_boundary();
5248                    None
5249                };
5250                if is_database {
5251                    return Ok(Statement::NoOpPreventedInTransaction {
5252                        what: String::from("CREATE DATABASE"),
5253                        collation,
5254                        name,
5255                    });
5256                }
5257                Ok(Statement::Empty)
5258            }
5259            // v7.39 (round 706) — the foreign-data family leaves the silent
5260            // list: `CREATE SERVER …`, `CREATE FOREIGN TABLE …`, `CREATE
5261            // FOREIGN DATA WRAPPER …` are still consumed whole (SPG has no
5262            // FDW machinery), but the ENGINE now warns, so a restore log
5263            // says what will not function instead of reporting success.
5264            Token::Ident(s) | Token::QuotedIdent(s)
5265                if s.eq_ignore_ascii_case("server") || s.eq_ignore_ascii_case("foreign") =>
5266            {
5267                self.consume_until_statement_boundary();
5268                Ok(Statement::ValidateOnly {
5269                    kind: crate::ast::ValidateOnlyKind::ForeignInfra,
5270                    names: Vec::new(),
5271                })
5272            }
5273            other => Err(self.err(format!(
5274                "expected TABLE / INDEX / USER / EXTENSION / PUBLICATION / SUBSCRIPTION / FUNCTION / TRIGGER / SEQUENCE / SCHEMA / VIEW / TYPE / DOMAIN [OR REPLACE …] after CREATE, got {other:?}"
5275            ))),
5276        }
5277    }
5278
5279    /// v7.12.4 — `CREATE OR REPLACE` already consumed; the next
5280    /// keyword decides whether we parse a function or trigger
5281    /// body. PG accepts other `OR REPLACE`-able objects (VIEW,
5282    /// PROCEDURE) — those land in later releases.
5283    fn parse_create_function_or_trigger_after_or_replace(
5284        &mut self,
5285        or_replace: bool,
5286    ) -> Result<Statement, ParseError> {
5287        let tok = self.peek();
5288        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5289            return Err(self.err(alloc::format!(
5290                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {tok:?}"
5291            )));
5292        };
5293        if s.eq_ignore_ascii_case("function") {
5294            self.advance();
5295            self.parse_create_function_after_keyword(or_replace)
5296        } else if s.eq_ignore_ascii_case("trigger") {
5297            self.advance();
5298            self.parse_create_trigger_after_keyword(or_replace)
5299        } else if s.eq_ignore_ascii_case("rule") {
5300            // v7.39 (round 143) — CREATE OR REPLACE RULE name AS ON …
5301            self.advance();
5302            self.parse_create_rule_after_keyword(or_replace)
5303        } else if s.eq_ignore_ascii_case("view") {
5304            // v7.17.0 Phase 1.2 — CREATE OR REPLACE VIEW name AS SELECT …
5305            self.advance();
5306            self.parse_create_view_after_keyword(or_replace, false, false)
5307        } else if s.eq_ignore_ascii_case("temporary") || s.eq_ignore_ascii_case("temp") {
5308            // CREATE OR REPLACE TEMPORARY VIEW … (rare but legal).
5309            self.advance();
5310            let nxt = self.peek().clone();
5311            if matches!(&nxt, Token::Ident(n) | Token::QuotedIdent(n) if n.eq_ignore_ascii_case("view"))
5312            {
5313                self.advance();
5314                self.parse_create_view_after_keyword(or_replace, false, true)
5315            } else {
5316                Err(self.err(alloc::format!(
5317                    "expected VIEW after CREATE OR REPLACE TEMPORARY, got {nxt:?}"
5318                )))
5319            }
5320        } else {
5321            Err(self.err(alloc::format!(
5322                "expected FUNCTION / TRIGGER / RULE / VIEW after CREATE OR REPLACE, got {s:?}"
5323            )))
5324        }
5325    }
5326
5327    /// v7.9.15 — accept and discard `CREATE EXTENSION` DDL.
5328    /// SPG doesn't have a registry; pgvector / similar are
5329    /// either builtin (VECTOR(N) ↔ pgvector) or n/a. Parsing
5330    /// the syntax lets dual-target schemas keep the line.
5331    fn parse_create_extension_after_keyword(&mut self) -> Result<Statement, ParseError> {
5332        // Optional `IF NOT EXISTS`.
5333        self.consume_if_not_exists();
5334        let name = self.expect_ident_like()?;
5335        // Drain optional WITH SCHEMA <ident> / VERSION '<v>' /
5336        // CASCADE / FROM '<v>' clauses; we don't model them.
5337        loop {
5338            match self.peek() {
5339                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
5340                    self.advance();
5341                    continue;
5342                }
5343                Token::Ident(s) if s.eq_ignore_ascii_case("schema") => {
5344                    self.advance();
5345                    let _ = self.expect_ident_like()?;
5346                    continue;
5347                }
5348                Token::Ident(s) if s.eq_ignore_ascii_case("version") => {
5349                    self.advance();
5350                    // String or ident literal.
5351                    let _ = self.advance();
5352                    continue;
5353                }
5354                Token::Ident(s) if s.eq_ignore_ascii_case("from") => {
5355                    self.advance();
5356                    let _ = self.advance();
5357                    continue;
5358                }
5359                Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => {
5360                    self.advance();
5361                    continue;
5362                }
5363                _ => break,
5364            }
5365        }
5366        // v7.39 (round 697) — the NAME is checked now. `CREATE EXTENSION
5367        // nosuch` reported success and `pg_extension` then did not list it,
5368        // which is the accept-and-do-nothing shape F31 exists to find.
5369        Ok(Statement::ValidateOnly {
5370            kind: crate::ast::ValidateOnlyKind::ExtensionAvailable,
5371            names: alloc::vec![name],
5372        })
5373    }
5374
5375    /// v7.12.4 — body of `CREATE [OR REPLACE] FUNCTION`. The
5376    /// `[OR REPLACE]` flag (and the `FUNCTION` keyword) have
5377    /// already been consumed by the caller. Grammar accepted:
5378    ///
5379    ///   name `(` arg-list `)`
5380    ///   `RETURNS` return-type
5381    ///   [ `LANGUAGE` ident ]
5382    ///   `AS` $$ body $$
5383    ///   [ `LANGUAGE` ident ]
5384    ///
5385    /// Either `LANGUAGE` position is allowed; PG accepts both.
5386    fn parse_create_function_after_keyword(
5387        &mut self,
5388        or_replace: bool,
5389    ) -> Result<Statement, ParseError> {
5390        let name = self.expect_ident_like()?;
5391        // Argument list. v7.12.4 commonly sees the empty `()`
5392        // (trigger functions); typed args parse and round-trip
5393        // but the executor only invokes nullary functions.
5394        if !matches!(self.peek(), Token::LParen) {
5395            return Err(self.err(alloc::format!(
5396                "expected '(' after function name {name:?}, got {:?}",
5397                self.peek()
5398            )));
5399        }
5400        self.advance();
5401        let args = self.parse_function_arg_list()?;
5402        // RETURNS clause.
5403        let tok = self.peek();
5404        let (Token::Ident(s) | Token::QuotedIdent(s)) = tok else {
5405            return Err(self.err(alloc::format!(
5406                "expected RETURNS after function arg list, got {tok:?}"
5407            )));
5408        };
5409        if !s.eq_ignore_ascii_case("returns") {
5410            return Err(self.err(alloc::format!(
5411                "expected RETURNS after function arg list, got {s:?}"
5412            )));
5413        }
5414        self.advance();
5415        let returns = self.parse_function_return()?;
5416        // Optional LANGUAGE clause (PG also accepts after AS — we'll
5417        // re-check after the body too).
5418        let mut language: Option<String> = self.parse_optional_language()?;
5419        // v7.39 (round 322, V46) — attribute clauses. PG allows them on
5420        // either side of the body and in any order, interleaved with
5421        // LANGUAGE; `CREATE FUNCTION f() RETURNS int LANGUAGE sql
5422        // IMMUTABLE STRICT AS $$…$$` used to be a parse error, which meant
5423        // PG's own pg_dump output did not restore.
5424        let mut attrs = FunctionAttrs::default();
5425        loop {
5426            let before = self.pos;
5427            self.parse_function_attrs_into(&mut attrs)?;
5428            if language.is_none() {
5429                language = self.parse_optional_language()?;
5430            }
5431            if self.pos == before {
5432                break;
5433            }
5434        }
5435        // `AS` followed by a $$-quoted body (lexer already
5436        // collapses both `$$…$$` and `$tag$…$tag$` to a single
5437        // Token::String). AS is a reserved keyword (Token::As).
5438        if !matches!(self.peek(), Token::As) {
5439            return Err(self.err(alloc::format!(
5440                "expected AS before function body, got {:?}",
5441                self.peek()
5442            )));
5443        }
5444        self.advance();
5445        let body_text = match self.peek() {
5446            Token::String(s) => {
5447                let body = s.clone();
5448                self.advance();
5449                body
5450            }
5451            other => {
5452                return Err(self.err(alloc::format!(
5453                    "expected $$-quoted function body after AS, got {other:?}"
5454                )));
5455            }
5456        };
5457        // Trailing clauses — PG's other accepted position for both the
5458        // LANGUAGE and the attributes.
5459        loop {
5460            let before = self.pos;
5461            self.parse_function_attrs_into(&mut attrs)?;
5462            if language.is_none() {
5463                language = self.parse_optional_language()?;
5464            }
5465            if self.pos == before {
5466                break;
5467            }
5468        }
5469        let language = language.unwrap_or_else(|| String::from("sql"));
5470        // PL/pgSQL bodies get structure-parsed. Other languages
5471        // (or PL/pgSQL bodies the v7.12.4 parser doesn't yet
5472        // recognise) round-trip as Raw text — the executor errors
5473        // when invoked with a clear unsupported message.
5474        let body = if language.eq_ignore_ascii_case("plpgsql") {
5475            match parse_plpgsql_body(&body_text) {
5476                Ok(block) => FunctionBody::PlPgSql(block),
5477                // Best-effort: if the body parser doesn't yet
5478                // support a construct used inside, fall back to
5479                // raw — keeps `CREATE FUNCTION` itself working
5480                // (catalogue accepts), executor errors on
5481                // invocation only.
5482                Err(_) => FunctionBody::Raw(body_text),
5483            }
5484        } else {
5485            FunctionBody::Raw(body_text)
5486        };
5487        Ok(Statement::CreateFunction(CreateFunctionStatement {
5488            name,
5489            or_replace,
5490            args,
5491            returns,
5492            language,
5493            body,
5494            attrs,
5495        }))
5496    }
5497
5498    /// v7.39 (round 322, V46) — consume any run of `CREATE FUNCTION`
5499    /// attribute clauses into `attrs`, stopping at the first token that
5500    /// is not one. Measured against PG 18.4, which accepts them in any
5501    /// order and on either side of the body.
5502    fn parse_function_attrs_into(&mut self, attrs: &mut FunctionAttrs) -> Result<(), ParseError> {
5503        loop {
5504            let word = match self.peek() {
5505                Token::Ident(w) | Token::QuotedIdent(w) => w.to_ascii_lowercase(),
5506                // NOT LEAKPROOF — NOT is a reserved keyword token.
5507                Token::Not
5508                    if matches!(
5509                        self.tokens.get(self.pos + 1),
5510                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("leakproof")
5511                    ) =>
5512                {
5513                    self.advance();
5514                    self.advance();
5515                    attrs.leakproof = false;
5516                    continue;
5517                }
5518                _ => return Ok(()),
5519            };
5520            match word.as_str() {
5521                "immutable" => {
5522                    self.advance();
5523                    attrs.volatility = FunctionVolatility::Immutable;
5524                }
5525                "stable" => {
5526                    self.advance();
5527                    attrs.volatility = FunctionVolatility::Stable;
5528                }
5529                "volatile" => {
5530                    self.advance();
5531                    attrs.volatility = FunctionVolatility::Volatile;
5532                }
5533                "strict" => {
5534                    self.advance();
5535                    attrs.strict = true;
5536                }
5537                "leakproof" => {
5538                    self.advance();
5539                    attrs.leakproof = true;
5540                }
5541                // RETURNS NULL ON NULL INPUT / CALLED ON NULL INPUT — the
5542                // spelled-out forms of STRICT and its opposite.
5543                "returns" | "called" => {
5544                    let strict = word == "returns";
5545                    let mut probe = self.pos + 1;
5546                    if strict {
5547                        // RETURNS *NULL* ON NULL INPUT; a bare RETURNS here
5548                        // is not ours.
5549                        match self.tokens.get(probe) {
5550                            Some(Token::Null) => probe += 1,
5551                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5552                            _ => return Ok(()),
5553                        }
5554                    }
5555                    let ok = matches!(self.tokens.get(probe), Some(Token::On))
5556                        || matches!(self.tokens.get(probe), Some(Token::Ident(w)) if w.eq_ignore_ascii_case("on"));
5557                    if !ok {
5558                        return Ok(());
5559                    }
5560                    probe += 1;
5561                    match self.tokens.get(probe) {
5562                        Some(Token::Null) => probe += 1,
5563                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("null") => probe += 1,
5564                        _ => return Ok(()),
5565                    }
5566                    match self.tokens.get(probe) {
5567                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("input") => probe += 1,
5568                        _ => return Ok(()),
5569                    }
5570                    self.pos = probe;
5571                    attrs.strict = strict;
5572                }
5573                "security" | "external" => {
5574                    // [EXTERNAL] SECURITY { INVOKER | DEFINER }
5575                    let mut probe = self.pos + 1;
5576                    if word == "external" {
5577                        match self.tokens.get(probe) {
5578                            Some(Token::Ident(w)) if w.eq_ignore_ascii_case("security") => {
5579                                probe += 1;
5580                            }
5581                            _ => return Ok(()),
5582                        }
5583                    }
5584                    let definer = match self.tokens.get(probe) {
5585                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("definer") => true,
5586                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("invoker") => false,
5587                        _ => return Ok(()),
5588                    };
5589                    self.pos = probe + 1;
5590                    attrs.security_definer = definer;
5591                }
5592                "parallel" => {
5593                    let level = match self.tokens.get(self.pos + 1) {
5594                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("safe") => {
5595                            FunctionParallel::Safe
5596                        }
5597                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("restricted") => {
5598                            FunctionParallel::Restricted
5599                        }
5600                        Some(Token::Ident(w)) if w.eq_ignore_ascii_case("unsafe") => {
5601                            FunctionParallel::Unsafe
5602                        }
5603                        _ => return Ok(()),
5604                    };
5605                    self.pos += 2;
5606                    attrs.parallel = level;
5607                }
5608                "cost" | "rows" => {
5609                    let Some(n) = self.peek_number_at(self.pos + 1) else {
5610                        return Ok(());
5611                    };
5612                    self.pos += 2;
5613                    if word == "cost" {
5614                        attrs.cost = Some(n);
5615                    } else {
5616                        attrs.rows = Some(n);
5617                    }
5618                }
5619                _ => return Ok(()),
5620            }
5621        }
5622    }
5623
5624    /// The numeric literal at `idx`, if there is one.
5625    fn peek_number_at(&self, idx: usize) -> Option<f64> {
5626        match self.tokens.get(idx)? {
5627            Token::Integer(n) => Some(*n as f64),
5628            Token::Float(f) => Some(*f),
5629            Token::Numeric(t) => t.parse::<f64>().ok(),
5630            _ => None,
5631        }
5632    }
5633
5634    /// Closing `)`-terminated argument list. v7.12.4 commonly
5635    /// sees the empty `()`; typed args round-trip but the
5636    /// executor (yet) doesn't invoke them.
5637    /// v7.39 (round 344) — consume a `( n [, m] )` type modifier and throw
5638    /// it away, which is what PG does with one on a function parameter.
5639    fn skip_type_modifier(&mut self) {
5640        if !matches!(self.peek(), Token::LParen) {
5641            return;
5642        }
5643        // Only a numeric modifier — anything else is not one, and eating
5644        // it would swallow real grammar.
5645        let mut i = self.pos + 1;
5646        let mut seen_number = false;
5647        loop {
5648            match self.tokens.get(i) {
5649                Some(Token::Integer(_)) => seen_number = true,
5650                Some(Token::Comma) => {}
5651                Some(Token::RParen) => break,
5652                _ => return,
5653            }
5654            i += 1;
5655        }
5656        if !seen_number {
5657            return;
5658        }
5659        while self.pos <= i {
5660            self.advance();
5661        }
5662    }
5663
5664    fn parse_function_arg_list(&mut self) -> Result<Vec<FunctionArg>, ParseError> {
5665        let mut args: Vec<FunctionArg> = Vec::new();
5666        if matches!(self.peek(), Token::RParen) {
5667            self.advance();
5668            return Ok(args);
5669        }
5670        loop {
5671            // Optional `IN` / `OUT` / `INOUT` mode keyword. IN is
5672            // a reserved token; OUT / INOUT are bare idents.
5673            let mode = if matches!(self.peek(), Token::In) {
5674                self.advance();
5675                FunctionArgMode::In
5676            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("out"))
5677            {
5678                self.advance();
5679                FunctionArgMode::Out
5680            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("inout"))
5681            {
5682                self.advance();
5683                FunctionArgMode::InOut
5684            } else {
5685                FunctionArgMode::In
5686            };
5687            // Optional name. The next token is either a name
5688            // (followed by a type ident) or the type itself.
5689            // Disambiguate by peeking ahead: if the token after
5690            // the next ident is also an ident, we treat the
5691            // first as the name.
5692            // v7.39 (round 315, V19) — take EVERY ident-like word up to
5693            // the comma or paren, then decide. Reading at most two of
5694            // them could not spell `x double precision` at all, and
5695            // silently mis-read the bare `double precision` as a
5696            // parameter named "double" — which is what made the same
5697            // signature key two different ways.
5698            let (name, ty_token) = {
5699                let mut words: Vec<String> = alloc::vec![self.expect_ident_like()?];
5700                while matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
5701                    words.push(self.expect_ident_like()?);
5702                }
5703                // v7.39 (round 344) — a length / precision modifier on the
5704                // type: `f(character varying(9))`, `f(numeric(10,2))`. PG
5705                // accepts it and DROPS it — `pg_get_function_arguments`
5706                // reports plain `character varying` / `numeric`, measured on
5707                // 18.4 — but SPG raised `syntax error at or near "("`,
5708                // because the modifier's parens were never consumed.
5709                self.skip_type_modifier();
5710                // r1049 — `f(v bigint[])`. The array suffix parsed in
5711                // the column position, the cast position and (r1038)
5712                // the RETURNS position, but not here: the fifth
5713                // member of the same family, reported by sentori as
5714                // presumably the same code. It is now.
5715                let array_suffix = self.consume_array_suffix();
5716                let whole = words.join(" ");
5717                let (name, mut ty_token) = if words.len() >= 2 && !is_multiword_type_phrase(&whole)
5718                {
5719                    (Some(words[0].clone()), words[1..].join(" "))
5720                } else {
5721                    (None, whole)
5722                };
5723                ty_token.push_str(&array_suffix);
5724                (name, ty_token)
5725            };
5726            // Type — try to map to ColumnTypeName, else Raw.
5727            let ty = match map_type_ident_to_column_type_name(&ty_token) {
5728                Some(t) => FunctionArgType::Typed(t),
5729                None => FunctionArgType::Raw(ty_token),
5730            };
5731            args.push(FunctionArg { mode, name, ty });
5732            match self.peek() {
5733                Token::Comma => {
5734                    self.advance();
5735                    continue;
5736                }
5737                Token::RParen => {
5738                    self.advance();
5739                    return Ok(args);
5740                }
5741                other => {
5742                    return Err(self.err(alloc::format!(
5743                        "expected , or ) in function arg list, got {other:?}"
5744                    )));
5745                }
5746            }
5747        }
5748    }
5749
5750    fn parse_function_return(&mut self) -> Result<FunctionReturn, ParseError> {
5751        // v7.39 (read01 round 65) — `RETURNS TABLE(col type, …)`: a set-returning
5752        // function whose row shape is named inline.
5753        if matches!(self.peek(), Token::Table)
5754            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
5755        {
5756            self.advance(); // TABLE
5757            self.advance(); // (
5758            let mut cols: Vec<String> = Vec::new();
5759            loop {
5760                let cname = self.expect_ident_like()?;
5761                let mut ty: Vec<String> = Vec::new();
5762                loop {
5763                    match self.peek() {
5764                        Token::Comma | Token::RParen | Token::Eof => break,
5765                        _ => {}
5766                    }
5767                    match self.advance() {
5768                        Token::Ident(w) | Token::QuotedIdent(w) => ty.push(w),
5769                        other => {
5770                            if let Some(w) = unreserved_keyword_text(&other) {
5771                                ty.push(w);
5772                            }
5773                        }
5774                    }
5775                }
5776                cols.push(alloc::format!("{cname} {}", ty.join(" ")));
5777                if matches!(self.peek(), Token::Comma) {
5778                    self.advance();
5779                } else {
5780                    break;
5781                }
5782            }
5783            if matches!(self.peek(), Token::RParen) {
5784                self.advance();
5785            }
5786            return Ok(FunctionReturn::Other(alloc::format!(
5787                "TABLE({})",
5788                cols.join(", ")
5789            )));
5790        }
5791        let ident = self.expect_ident_like()?;
5792        // v7.39 (read01 round 65) — `RETURNS SETOF <type>`.
5793        if ident.eq_ignore_ascii_case("setof") {
5794            let inner = self.expect_ident_like()?;
5795            let inner = alloc::format!("{inner}{}", self.consume_array_suffix());
5796            return Ok(FunctionReturn::Other(alloc::format!("SETOF {inner}")));
5797        }
5798        if ident.eq_ignore_ascii_case("trigger") {
5799            return Ok(FunctionReturn::Trigger);
5800        }
5801        if ident.eq_ignore_ascii_case("void") {
5802            return Ok(FunctionReturn::Void);
5803        }
5804        // r1038 — `RETURNS bigint[]`. An array COLUMN type parsed; the
5805        // RETURN position did not, so the `[` was a syntax error and the
5806        // whole migration stopped. sentori worked around it by returning
5807        // zero-padded text.
5808        let suffix = self.consume_array_suffix();
5809        if !suffix.is_empty() {
5810            return Ok(FunctionReturn::Other(alloc::format!("{ident}{suffix}")));
5811        }
5812        match map_type_ident_to_column_type_name(&ident) {
5813            Some(t) => Ok(FunctionReturn::Type(t)),
5814            None => Ok(FunctionReturn::Other(ident)),
5815        }
5816    }
5817
5818    /// Consume any `[]` / `[N]` array markers after a type name and give
5819    /// back their text. Empty when there are none.
5820    fn consume_array_suffix(&mut self) -> String {
5821        let mut out = String::new();
5822        while matches!(self.peek(), Token::LBracket) {
5823            self.advance();
5824            // `[N]` is accepted and, as in PG, the length is not enforced.
5825            if let Token::Integer(n) = self.peek().clone() {
5826                self.advance();
5827                out.push_str(&alloc::format!("[{n}]"));
5828            } else {
5829                out.push_str("[]");
5830            }
5831            if matches!(self.peek(), Token::RBracket) {
5832                self.advance();
5833            }
5834        }
5835        out
5836    }
5837
5838    fn parse_optional_language(&mut self) -> Result<Option<String>, ParseError> {
5839        match self.peek() {
5840            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("language") => {
5841                self.advance();
5842                let lang = self.expect_ident_like()?;
5843                Ok(Some(lang.to_ascii_lowercase()))
5844            }
5845            _ => Ok(None),
5846        }
5847    }
5848
5849    /// v7.17.0 Phase 1.5 — body of `CREATE DOMAIN name AS
5850    /// base_type [DEFAULT expr] [NOT NULL | NULL] [CHECK
5851    /// (expr)]*`. The `DOMAIN` keyword has already been
5852    /// consumed. PG allows the trailing constraints in any
5853    /// order; we approximate with a small loop.
5854    fn parse_create_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
5855        let name = self.expect_ident_like()?;
5856        // Optional `AS`.
5857        if matches!(self.peek(), Token::As) {
5858            self.advance();
5859        }
5860        // v7.39 (round 259) — keep the raw type NAME when the base is not
5861        // a builtin: it is how `CREATE DOMAIN child AS parent` records its
5862        // parent domain.
5863        let (base_type, _, _, base_user_ref, _, _, _, _, _, _, _, _, _, _) =
5864            self.parse_type_with_implied_flags()?;
5865        let mut default: Option<Expr> = None;
5866        let mut not_null = false;
5867        let mut checks: Vec<Expr> = Vec::new();
5868        loop {
5869            match self.peek() {
5870                Token::Default => {
5871                    if default.is_some() {
5872                        return Err(self.err("DOMAIN DEFAULT specified twice".into()));
5873                    }
5874                    self.advance();
5875                    default = Some(self.parse_expr(0)?);
5876                }
5877                Token::Not => {
5878                    self.advance();
5879                    if !matches!(self.peek(), Token::Null) {
5880                        return Err(self.err(alloc::format!(
5881                            "expected NULL after NOT in DOMAIN, got {:?}",
5882                            self.peek()
5883                        )));
5884                    }
5885                    self.advance();
5886                    not_null = true;
5887                }
5888                Token::Null => {
5889                    self.advance();
5890                    // v7.39 (round 761, F31 tranche 2 #31) — bare NULL
5891                    // is the default-nullable marker (PG accepts it),
5892                    // but AFTER a NOT NULL it is a conflict PG refuses
5893                    // (`conflicting NULL/NOT NULL constraints`,
5894                    // PG18-measured); the old arm no-opped both ways.
5895                    if not_null {
5896                        return Err(self.err(alloc::string::String::from(
5897                            "conflicting NULL/NOT NULL constraints",
5898                        )));
5899                    }
5900                }
5901                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check") => {
5902                    self.advance();
5903                    if !matches!(self.peek(), Token::LParen) {
5904                        return Err(self.err(alloc::format!(
5905                            "expected '(' after CHECK in DOMAIN, got {:?}",
5906                            self.peek()
5907                        )));
5908                    }
5909                    self.advance();
5910                    let expr = self.parse_expr(0)?;
5911                    if !matches!(self.peek(), Token::RParen) {
5912                        return Err(self.err(alloc::format!(
5913                            "expected ')' after CHECK expr, got {:?}",
5914                            self.peek()
5915                        )));
5916                    }
5917                    self.advance();
5918                    checks.push(expr);
5919                }
5920                // CONSTRAINT <name> CHECK (…) — PG accepts a name
5921                // prefix on the constraint; we drop the name and
5922                // recurse into the constraint parsing.
5923                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
5924                    self.advance();
5925                    let _ = self.expect_ident_like()?;
5926                }
5927                _ => break,
5928            }
5929        }
5930        Ok(Statement::CreateDomain(crate::ast::CreateDomainStatement {
5931            name,
5932            base_type,
5933            base_domain: base_user_ref,
5934            default,
5935            not_null,
5936            checks,
5937        }))
5938    }
5939
5940    /// v7.17.0 Phase 1.4 — body of `CREATE TYPE name AS ENUM
5941    /// ('a', 'b', …)`. The `TYPE` keyword has already been
5942    /// consumed.
5943    fn parse_create_type_after_keyword(&mut self) -> Result<Statement, ParseError> {
5944        let name = self.expect_ident_like()?;
5945        // Required `AS`.
5946        if !matches!(self.peek(), Token::As) {
5947            return Err(self.err(alloc::format!(
5948                "expected AS after CREATE TYPE {name:?}, got {:?}",
5949                self.peek()
5950            )));
5951        }
5952        self.advance();
5953        // v7.37.x (ζ-B composite Phase 1) — `AS (` is the composite-
5954        // type shape: `CREATE TYPE foo AS (a INT, b TEXT)`. Branch
5955        // on the next token: `(` = composite, ident `ENUM` = enum.
5956        if matches!(self.peek(), Token::LParen) {
5957            self.advance();
5958            let mut fields: Vec<(String, ColumnTypeName)> = Vec::new();
5959            let mut field_user_types: Vec<Option<String>> = Vec::new();
5960            // v7.39 (round 769, F31 tranche 5 #140) — `CREATE TYPE x AS ()`
5961            // is legal PG (an attribute-less composite; measured — the old
5962            // e2e note claimed PG requires at least one attribute).
5963            if matches!(self.peek(), Token::RParen) {
5964                self.advance();
5965                return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5966                    name,
5967                    kind: crate::ast::TypeKind::Composite {
5968                        fields,
5969                        field_user_types,
5970                    },
5971                }));
5972            }
5973            loop {
5974                let field_name = self.expect_ident_like()?;
5975                // v7.39 (round 264) — keep the raw type name when it is not
5976                // a builtin: that is how a NESTED composite field records
5977                // which composite it holds.
5978                let (field_type, _, _, field_user_ref, _, _, _, _, _, _, _, _, _, _) =
5979                    self.parse_type_with_implied_flags()?;
5980                fields.push((field_name, field_type));
5981                field_user_types.push(field_user_ref);
5982                if matches!(self.peek(), Token::Comma) {
5983                    self.advance();
5984                    continue;
5985                }
5986                if matches!(self.peek(), Token::RParen) {
5987                    self.advance();
5988                    break;
5989                }
5990                return Err(self.err(alloc::format!(
5991                    "expected , or ) in composite field list, got {:?}",
5992                    self.peek()
5993                )));
5994            }
5995            if fields.is_empty() {
5996                return Err(self.err("CREATE TYPE … AS (…) must declare at least one field".into()));
5997            }
5998            return Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
5999                name,
6000                kind: crate::ast::TypeKind::Composite {
6001                    fields,
6002                    field_user_types,
6003                },
6004            }));
6005        }
6006        // Required `ENUM` ident.
6007        let kind_ident = match self.peek().clone() {
6008            Token::Ident(s) | Token::QuotedIdent(s) => s,
6009            other => {
6010                return Err(self.err(alloc::format!(
6011                    "expected ENUM or '(' after CREATE TYPE {name:?} AS, got {other:?}"
6012                )));
6013            }
6014        };
6015        if !kind_ident.eq_ignore_ascii_case("enum") {
6016            return Err(self.err(alloc::format!(
6017                "Phase 1.4 only supports ENUM or composite '(…)'; got {kind_ident:?}"
6018            )));
6019        }
6020        self.advance();
6021        if !matches!(self.peek(), Token::LParen) {
6022            return Err(self.err(alloc::format!(
6023                "expected '(' after ENUM, got {:?}",
6024                self.peek()
6025            )));
6026        }
6027        self.advance();
6028        let mut labels: Vec<String> = Vec::new();
6029        loop {
6030            match self.peek().clone() {
6031                Token::String(s) => {
6032                    self.advance();
6033                    labels.push(s);
6034                }
6035                other => {
6036                    return Err(
6037                        self.err(alloc::format!("expected enum label string, got {other:?}"))
6038                    );
6039                }
6040            }
6041            if matches!(self.peek(), Token::Comma) {
6042                self.advance();
6043                continue;
6044            }
6045            if matches!(self.peek(), Token::RParen) {
6046                self.advance();
6047                break;
6048            }
6049            return Err(self.err(alloc::format!(
6050                "expected , or ) in ENUM label list, got {:?}",
6051                self.peek()
6052            )));
6053        }
6054        if labels.is_empty() {
6055            return Err(self.err("CREATE TYPE … AS ENUM must declare at least one label".into()));
6056        }
6057        Ok(Statement::CreateType(crate::ast::CreateTypeStatement {
6058            name,
6059            kind: crate::ast::TypeKind::Enum { labels },
6060        }))
6061    }
6062
6063    /// v7.17.0 Phase 1.3 — body of `CREATE MATERIALIZED VIEW
6064    /// [IF NOT EXISTS] name [(col, …)] AS <SELECT …> [WITH [NO] DATA]`.
6065    /// The `CREATE MATERIALIZED VIEW` keywords have already been
6066    /// consumed.
6067    fn parse_create_materialized_view_after_keyword(&mut self) -> Result<Statement, ParseError> {
6068        let if_not_exists = self.parse_if_not_exists();
6069        let name = self.expect_ident_like()?;
6070        let mut columns: Vec<String> = Vec::new();
6071        if matches!(self.peek(), Token::LParen) {
6072            self.advance();
6073            loop {
6074                let c = self.expect_ident_like()?;
6075                columns.push(c);
6076                if matches!(self.peek(), Token::Comma) {
6077                    self.advance();
6078                    continue;
6079                }
6080                if matches!(self.peek(), Token::RParen) {
6081                    self.advance();
6082                    break;
6083                }
6084                return Err(self.err(alloc::format!(
6085                    "expected , or ) in MATERIALIZED VIEW column list, got {:?}",
6086                    self.peek()
6087                )));
6088            }
6089        }
6090        if !matches!(self.peek(), Token::As) {
6091            return Err(self.err(alloc::format!(
6092                "expected AS <SELECT …> after CREATE MATERIALIZED VIEW {name:?}, got {:?}",
6093                self.peek()
6094            )));
6095        }
6096        self.advance();
6097        // v7.39 (round 151) — a WITH-headed body is legal (read-only
6098        // CTEs only; the engine rejects data-modifying ones with PG's
6099        // message). A trailing `WITH [NO] DATA` can't START the body,
6100        // so WITH here heads the query.
6101        let body = if self.peek_is_with_kw() {
6102            self.advance();
6103            self.parse_nested_with_select()?
6104        } else {
6105            let body_stmt = self.parse_select_stmt()?;
6106            let Statement::Select(body) = body_stmt else {
6107                return Err(self.err(alloc::format!(
6108                    "CREATE MATERIALIZED VIEW body must be a SELECT, got {body_stmt:?}"
6109                )));
6110            };
6111            body
6112        };
6113        // Optional trailing `WITH [NO] DATA`.
6114        let with_data = self.parse_optional_with_data(true)?;
6115        Ok(Statement::CreateMaterializedView(
6116            crate::ast::CreateMaterializedViewStatement {
6117                temporary: false,
6118                name,
6119                if_not_exists,
6120                columns,
6121                body,
6122                with_data,
6123                as_plain_table: false,
6124            },
6125        ))
6126    }
6127
6128    /// v7.17.0 Phase 1.3 — `WITH [NO] DATA` trailer.
6129    /// `default_when_absent` is what to return if the tail is
6130    /// missing (CREATE defaults to WITH DATA, REFRESH defaults to
6131    /// WITH DATA).
6132    fn parse_optional_with_data(&mut self, default_when_absent: bool) -> Result<bool, ParseError> {
6133        let save = self.pos;
6134        // `WITH` is an Ident (not reserved in the lexer).
6135        let is_with = match self.peek() {
6136            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("with"),
6137            _ => false,
6138        };
6139        if !is_with {
6140            return Ok(default_when_absent);
6141        }
6142        self.advance();
6143        // Optional `NO`.
6144        let mut with_data = true;
6145        let is_no = match self.peek() {
6146            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("no"),
6147            _ => false,
6148        };
6149        if is_no {
6150            self.advance();
6151            with_data = false;
6152        }
6153        // Required `DATA` ident.
6154        let is_data = match self.peek() {
6155            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("data"),
6156            _ => false,
6157        };
6158        if is_data {
6159            self.advance();
6160            Ok(with_data)
6161        } else {
6162            // Caller's WITH wasn't WITH-DATA — rewind so the outer
6163            // parser can interpret it.
6164            self.pos = save;
6165            Ok(default_when_absent)
6166        }
6167    }
6168
6169    /// v7.17.0 Phase 1.2 — body of `CREATE [OR REPLACE]
6170    /// [TEMPORARY] VIEW [IF NOT EXISTS] name [(col, …)] AS <SELECT>`.
6171    /// All keyword prefixes have already been consumed; the flags
6172    /// say which were present.
6173    fn parse_create_view_after_keyword(
6174        &mut self,
6175        or_replace: bool,
6176        _materialized_unused: bool,
6177        temporary: bool,
6178    ) -> Result<Statement, ParseError> {
6179        let if_not_exists = self.parse_if_not_exists();
6180        let name = self.expect_ident_like()?;
6181        // Optional `(col, col, …)` rename list.
6182        let mut columns: Vec<String> = Vec::new();
6183        if matches!(self.peek(), Token::LParen) {
6184            self.advance();
6185            loop {
6186                let c = self.expect_ident_like()?;
6187                columns.push(c);
6188                if matches!(self.peek(), Token::Comma) {
6189                    self.advance();
6190                    continue;
6191                }
6192                if matches!(self.peek(), Token::RParen) {
6193                    self.advance();
6194                    break;
6195                }
6196                return Err(self.err(alloc::format!(
6197                    "expected , or ) in VIEW column list, got {:?}",
6198                    self.peek()
6199                )));
6200            }
6201        }
6202        // Required `AS`.
6203        if !matches!(self.peek(), Token::As) {
6204            return Err(self.err(alloc::format!(
6205                "expected AS <SELECT …> after CREATE VIEW {name:?}, got {:?}",
6206                self.peek()
6207            )));
6208        }
6209        self.advance();
6210        // Body: a regular SELECT statement. v7.39 (round 151) — a
6211        // WITH-headed body is legal too (read-only CTEs only; the
6212        // engine rejects data-modifying ones with PG's message).
6213        // Disambiguation vs `WITH CHECK OPTION`: a body can't START
6214        // with the check-option clause, so WITH here heads the query.
6215        let body = if self.peek_is_with_kw() {
6216            self.advance();
6217            self.parse_nested_with_select()?
6218        } else {
6219            let body_stmt = self.parse_select_stmt()?;
6220            let Statement::Select(body) = body_stmt else {
6221                return Err(self.err(alloc::format!(
6222                    "CREATE VIEW body must be a SELECT statement, got {body_stmt:?}"
6223                )));
6224            };
6225            body
6226        };
6227        // v7.39 (round 132) — optional `WITH [ LOCAL | CASCADED ] CHECK OPTION`.
6228        // The SELECT parser stops before a trailing WITH, so it lands here.
6229        let check_option = if matches!(self.peek(),
6230            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
6231        {
6232            self.advance(); // WITH
6233            let opt = match self.peek() {
6234                Token::Ident(s) if s.eq_ignore_ascii_case("local") => {
6235                    self.advance();
6236                    crate::ast::ViewCheckOption::Local
6237                }
6238                Token::Ident(s) if s.eq_ignore_ascii_case("cascaded") => {
6239                    self.advance();
6240                    crate::ast::ViewCheckOption::Cascaded
6241                }
6242                // Bare `WITH CHECK OPTION` defaults to CASCADED (PG).
6243                _ => crate::ast::ViewCheckOption::Cascaded,
6244            };
6245            if !matches!(self.peek(),
6246                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
6247            {
6248                return Err(self.err(alloc::format!(
6249                    "expected CHECK in CREATE VIEW … WITH [LOCAL|CASCADED] CHECK OPTION, got {:?}",
6250                    self.peek()
6251                )));
6252            }
6253            self.advance(); // CHECK
6254            if !matches!(self.peek(),
6255                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("option"))
6256            {
6257                return Err(self.err(alloc::format!(
6258                    "expected OPTION after WITH CHECK in CREATE VIEW, got {:?}",
6259                    self.peek()
6260                )));
6261            }
6262            self.advance(); // OPTION
6263            Some(opt)
6264        } else {
6265            None
6266        };
6267        Ok(Statement::CreateView(crate::ast::CreateViewStatement {
6268            name,
6269            or_replace,
6270            if_not_exists,
6271            temporary,
6272            columns,
6273            body,
6274            check_option,
6275        }))
6276    }
6277
6278    /// v7.17.0 — body of `CREATE [TEMPORARY] SEQUENCE`. The
6279    /// `[TEMPORARY]` and `SEQUENCE` tokens have already been
6280    /// consumed; `temporary` carries whether TEMPORARY was seen.
6281    fn parse_create_sequence_after_keyword(
6282        &mut self,
6283        temporary: bool,
6284    ) -> Result<Statement, ParseError> {
6285        let if_not_exists = self.parse_if_not_exists();
6286        let name = self.expect_ident_like()?;
6287        // Optional `AS data_type`.
6288        let data_type = if matches!(self.peek(), Token::As) {
6289            self.advance();
6290            Some(self.parse_sequence_data_type()?)
6291        } else {
6292            None
6293        };
6294        let options = self.parse_sequence_options(/* allow_restart = */ false)?;
6295        Ok(Statement::CreateSequence(
6296            crate::ast::CreateSequenceStatement {
6297                name,
6298                if_not_exists,
6299                temporary,
6300                data_type,
6301                options,
6302            },
6303        ))
6304    }
6305
6306    /// v7.17.0 — body of `ALTER SEQUENCE`. The `ALTER` keyword has
6307    /// already been consumed; this is reached after `SEQUENCE`.
6308    /// v7.39 (round 260) — `ALTER DOMAIN name <action>`.
6309    fn parse_alter_domain_after_keyword(&mut self) -> Result<Statement, ParseError> {
6310        use crate::ast::AlterDomainAction as A;
6311        let name = self.expect_ident_like()?;
6312        // DROP / SET / ADD lex as reserved keyword tokens, not idents.
6313        let kw = match self.peek() {
6314            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6315            Token::Drop => alloc::string::String::from("drop"),
6316            Token::Default => alloc::string::String::from("default"),
6317            other => {
6318                return Err(self.err(alloc::format!(
6319                    "expected an ALTER DOMAIN action, got {other:?}"
6320                )));
6321            }
6322        };
6323        let action = match kw.as_str() {
6324            "add" => {
6325                self.advance();
6326                let cname = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
6327                {
6328                    self.advance();
6329                    Some(self.expect_ident_like()?)
6330                } else {
6331                    None
6332                };
6333                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check")) {
6334                    return Err(self.err(alloc::format!(
6335                        "ALTER DOMAIN ADD supports CHECK only, got {:?}",
6336                        self.peek()
6337                    )));
6338                }
6339                self.advance();
6340                if !matches!(self.peek(), Token::LParen) {
6341                    return Err(self.err("expected '(' after CHECK".into()));
6342                }
6343                self.advance();
6344                let check = self.parse_expr(0)?;
6345                if !matches!(self.peek(), Token::RParen) {
6346                    return Err(self.err("expected ')' after CHECK expression".into()));
6347                }
6348                self.advance();
6349                A::AddConstraint { name: cname, check }
6350            }
6351            "drop" => {
6352                self.advance();
6353                match self.peek() {
6354                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
6355                        self.advance();
6356                        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
6357                        {
6358                            self.advance();
6359                            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists"))
6360                            {
6361                                return Err(self.err("expected EXISTS after IF".into()));
6362                            }
6363                            self.advance();
6364                            true
6365                        } else {
6366                            false
6367                        };
6368                        let cn = self.expect_ident_like()?;
6369                        A::DropConstraint {
6370                            name: cn,
6371                            if_exists,
6372                        }
6373                    }
6374                    Token::Default => {
6375                        self.advance();
6376                        A::DropDefault
6377                    }
6378                    Token::Not => {
6379                        self.advance();
6380                        if !matches!(self.peek(), Token::Null) {
6381                            return Err(self.err("expected NULL after NOT".into()));
6382                        }
6383                        self.advance();
6384                        A::DropNotNull
6385                    }
6386                    other => {
6387                        return Err(self.err(alloc::format!(
6388                            "ALTER DOMAIN DROP expects CONSTRAINT / DEFAULT / NOT NULL, got {other:?}"
6389                        )));
6390                    }
6391                }
6392            }
6393            "set" => {
6394                self.advance();
6395                match self.peek() {
6396                    Token::Default => {
6397                        self.advance();
6398                        A::SetDefault(self.parse_expr(0)?)
6399                    }
6400                    Token::Not => {
6401                        self.advance();
6402                        if !matches!(self.peek(), Token::Null) {
6403                            return Err(self.err("expected NULL after NOT".into()));
6404                        }
6405                        self.advance();
6406                        A::SetNotNull
6407                    }
6408                    other => {
6409                        return Err(self.err(alloc::format!(
6410                            "ALTER DOMAIN SET expects DEFAULT / NOT NULL, got {other:?}"
6411                        )));
6412                    }
6413                }
6414            }
6415            "rename" => {
6416                self.advance();
6417                if !matches!(self.peek(), Token::To) {
6418                    return Err(self.err("expected TO after RENAME".into()));
6419                }
6420                self.advance();
6421                A::RenameTo(self.expect_ident_like()?)
6422            }
6423            other => {
6424                return Err(self.err(alloc::format!("unsupported ALTER DOMAIN action {other:?}")));
6425            }
6426        };
6427        Ok(Statement::AlterDomain { name, action })
6428    }
6429
6430    fn parse_alter_sequence_after_keyword(&mut self) -> Result<Statement, ParseError> {
6431        let if_exists = self.parse_if_exists();
6432        let name = self.expect_ident_like()?;
6433        // v7.39 (read01 round 49) — `RENAME TO new`; mutually exclusive with
6434        // the option list (PG allows only one or the other).
6435        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
6436            self.advance();
6437            if matches!(self.peek(), Token::To) {
6438                self.advance();
6439            } else {
6440                self.expect_keyword_ident("to")?;
6441            }
6442            let new = self.expect_ident_like()?;
6443            return Ok(Statement::AlterSequence(
6444                crate::ast::AlterSequenceStatement {
6445                    name,
6446                    if_exists,
6447                    options: crate::ast::SequenceOptions::default(),
6448                    rename_to: Some(new),
6449                },
6450            ));
6451        }
6452        let options = self.parse_sequence_options(/* allow_restart = */ true)?;
6453        Ok(Statement::AlterSequence(
6454            crate::ast::AlterSequenceStatement {
6455                name,
6456                if_exists,
6457                options,
6458                rename_to: None,
6459            },
6460        ))
6461    }
6462
6463    fn parse_sequence_data_type(&mut self) -> Result<crate::ast::SequenceDataType, ParseError> {
6464        let kw = self.expect_ident_like()?;
6465        match kw.to_ascii_lowercase().as_str() {
6466            "smallint" | "int2" => Ok(crate::ast::SequenceDataType::SmallInt),
6467            "integer" | "int" | "int4" => Ok(crate::ast::SequenceDataType::Int),
6468            "bigint" | "int8" => Ok(crate::ast::SequenceDataType::BigInt),
6469            other => Err(self.err(alloc::format!(
6470                "expected SMALLINT / INTEGER / BIGINT after SEQUENCE AS, got {other:?}"
6471            ))),
6472        }
6473    }
6474
6475    fn parse_sequence_options(
6476        &mut self,
6477        allow_restart: bool,
6478    ) -> Result<crate::ast::SequenceOptions, ParseError> {
6479        use crate::ast::{SeqBound, SequenceOptions, SequenceOwnedBy};
6480        let mut opts = SequenceOptions::default();
6481        #[allow(clippy::while_let_loop)]
6482        loop {
6483            // Match an ident; stop at any non-ident token (sentinel,
6484            // semicolon, end of statement).
6485            let kw_lc = match self.peek() {
6486                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
6487                _ => break,
6488            };
6489            match kw_lc.as_str() {
6490                "increment" => {
6491                    self.advance();
6492                    // Optional BY.
6493                    if self.peek_is_by() {
6494                        self.advance();
6495                    }
6496                    opts.increment = Some(self.expect_signed_int()?);
6497                }
6498                "minvalue" => {
6499                    self.advance();
6500                    opts.min_value = Some(SeqBound::Value(self.expect_signed_int()?));
6501                }
6502                "maxvalue" => {
6503                    self.advance();
6504                    opts.max_value = Some(SeqBound::Value(self.expect_signed_int()?));
6505                }
6506                "no" => {
6507                    self.advance();
6508                    let what = self.expect_ident_like()?;
6509                    match what.to_ascii_lowercase().as_str() {
6510                        "minvalue" => opts.min_value = Some(SeqBound::NoBound),
6511                        "maxvalue" => opts.max_value = Some(SeqBound::NoBound),
6512                        "cycle" => opts.cycle = Some(false),
6513                        other => {
6514                            return Err(self.err(alloc::format!(
6515                                "expected MINVALUE / MAXVALUE / CYCLE after NO, got {other:?}"
6516                            )));
6517                        }
6518                    }
6519                }
6520                "start" => {
6521                    self.advance();
6522                    // Optional WITH.
6523                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6524                        if s.eq_ignore_ascii_case("with"))
6525                    {
6526                        self.advance();
6527                    }
6528                    opts.start = Some(self.expect_signed_int()?);
6529                }
6530                "restart" if allow_restart => {
6531                    self.advance();
6532                    // Optional WITH n; bare RESTART means restart at START.
6533                    let mut with_val: Option<i64> = None;
6534                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6535                        if s.eq_ignore_ascii_case("with"))
6536                    {
6537                        self.advance();
6538                        with_val = Some(self.expect_signed_int()?);
6539                    } else if matches!(self.peek(), Token::Integer(_) | Token::Minus) {
6540                        with_val = Some(self.expect_signed_int()?);
6541                    }
6542                    opts.restart = Some(with_val);
6543                }
6544                "cache" => {
6545                    self.advance();
6546                    opts.cache = Some(self.expect_signed_int()?);
6547                }
6548                "cycle" => {
6549                    self.advance();
6550                    opts.cycle = Some(true);
6551                }
6552                "owned" => {
6553                    self.advance();
6554                    match self.peek() {
6555                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("by") => {
6556                            self.advance();
6557                        }
6558                        other => {
6559                            return Err(
6560                                self.err(alloc::format!("expected BY after OWNED, got {other:?}"))
6561                            );
6562                        }
6563                    }
6564                    // OWNED BY {NONE | tab.col}. Read just one ident
6565                    // (NOT expect_ident_like which would auto-strip
6566                    // a schema prefix and consume the `.col` we need).
6567                    let first = match self.advance() {
6568                        Token::Ident(s) | Token::QuotedIdent(s) => s,
6569                        other => {
6570                            return Err(self.err(alloc::format!(
6571                                "expected identifier or NONE after OWNED BY, got {other:?}"
6572                            )));
6573                        }
6574                    };
6575                    if first.eq_ignore_ascii_case("none") {
6576                        opts.owned_by = Some(SequenceOwnedBy::None);
6577                    } else if matches!(self.peek(), Token::Dot) {
6578                        self.advance();
6579                        let second = match self.advance() {
6580                            Token::Ident(s) | Token::QuotedIdent(s) => s,
6581                            other => {
6582                                return Err(self.err(alloc::format!(
6583                                    "expected column name after OWNED BY {first}., got {other:?}"
6584                                )));
6585                            }
6586                        };
6587                        // v7.17 dump-compat fix — pg_dump emits
6588                        // OWNED BY clauses as
6589                        // `schema.table.column` (three segments).
6590                        // If a third `.<ident>` follows, treat the
6591                        // first ident as schema (drop it; SPG is
6592                        // single-schema) and the middle / last
6593                        // pair as table.column. Otherwise it's
6594                        // the two-segment form table.column.
6595                        if matches!(self.peek(), Token::Dot) {
6596                            self.advance();
6597                            let third = match self.advance() {
6598                                Token::Ident(s) | Token::QuotedIdent(s) => s,
6599                                other => {
6600                                    return Err(self.err(alloc::format!(
6601                                        "expected column name after OWNED BY {first}.{second}., got {other:?}"
6602                                    )));
6603                                }
6604                            };
6605                            let _ = first; // schema prefix discarded
6606                            opts.owned_by = Some(SequenceOwnedBy::Column {
6607                                table: second,
6608                                column: third,
6609                            });
6610                        } else {
6611                            opts.owned_by = Some(SequenceOwnedBy::Column {
6612                                table: first,
6613                                column: second,
6614                            });
6615                        }
6616                    } else {
6617                        return Err(self.err(alloc::format!(
6618                            "expected table.column or NONE after OWNED BY, got {first:?}"
6619                        )));
6620                    }
6621                }
6622                _ => break,
6623            }
6624        }
6625        Ok(opts)
6626    }
6627
6628    fn expect_signed_int(&mut self) -> Result<i64, ParseError> {
6629        let neg = if matches!(self.peek(), Token::Minus) {
6630            self.advance();
6631            true
6632        } else {
6633            false
6634        };
6635        match self.peek() {
6636            Token::Integer(n) => {
6637                let v = *n;
6638                self.advance();
6639                Ok(if neg { -v } else { v })
6640            }
6641            other => Err(self.err(alloc::format!("expected signed integer, got {other:?}"))),
6642        }
6643    }
6644
6645    /// v7.17.0 Phase 3.1 — absorb `[NOT] DEFERRABLE [INITIALLY
6646    /// {DEFERRED | IMMEDIATE}]` constraint-timing clauses. Each
6647    /// clause is fully accepted and discarded — SPG always runs
6648    /// constraint checks immediately (single-writer model). The
6649    /// loop allows DEFERRABLE and the INITIALLY suffix to appear
6650    /// in either order (per the SQL spec they're independent),
6651    /// though pg_dump always emits them in the canonical
6652    /// `[NOT] DEFERRABLE INITIALLY {DEFERRED|IMMEDIATE}` shape.
6653    /// Stops at the first token that isn't part of the clause.
6654    fn consume_optional_deferrable_clauses(&mut self) -> Result<(), ParseError> {
6655        self.consume_deferrable_clauses_timed().map(|_| ())
6656    }
6657
6658    /// v7.39 (round 288) — the same scan, but reporting what it saw:
6659    /// `(deferrable, initially_deferred)`. The clauses were parsed and
6660    /// dropped, so `DEFERRABLE INITIALLY DEFERRED` on an FK behaved as
6661    /// NOT DEFERRABLE and a circular-FK migration could not load.
6662    fn consume_deferrable_clauses_timed(&mut self) -> Result<(bool, bool), ParseError> {
6663        let mut deferrable = false;
6664        let mut initially_deferred = false;
6665        loop {
6666            // Bare `DEFERRABLE` (Phase 3.1 — was hard-error pre-3.1).
6667            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
6668                self.advance();
6669                deferrable = true;
6670                if self.consume_optional_initially_clause()? {
6671                    initially_deferred = true;
6672                }
6673                continue;
6674            }
6675            // `NOT DEFERRABLE` — already worked pre-3.1.
6676            if matches!(self.peek(), Token::Not) {
6677                let look = self.tokens.get(self.pos + 1);
6678                if matches!(look, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")) {
6679                    self.advance(); // NOT
6680                    self.advance(); // DEFERRABLE
6681                    deferrable = false;
6682                    initially_deferred = false;
6683                    let _ = self.consume_optional_initially_clause()?;
6684                    continue;
6685                }
6686                break;
6687            }
6688            // Standalone `INITIALLY {DEFERRED|IMMEDIATE}` — PG
6689            // accepts this without a leading [NOT] DEFERRABLE
6690            // (the timing keyword alone). pg_dump occasionally
6691            // emits it on FK constraints that inherit timing.
6692            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6693                if self.consume_optional_initially_clause()? {
6694                    initially_deferred = true;
6695                    // PG: a bare `INITIALLY DEFERRED` implies DEFERRABLE.
6696                    deferrable = true;
6697                }
6698                continue;
6699            }
6700            break;
6701        }
6702        Ok((deferrable, initially_deferred))
6703    }
6704
6705    /// Helper for [`consume_optional_deferrable_clauses`]. When the
6706    /// next token is `INITIALLY`, consume it plus the required
6707    /// `DEFERRED` | `IMMEDIATE` trailer. No-op otherwise.
6708    /// Returns true when the timing seen was `DEFERRED`.
6709    fn consume_optional_initially_clause(&mut self) -> Result<bool, ParseError> {
6710        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("initially")) {
6711            return Ok(false);
6712        }
6713        self.advance(); // INITIALLY
6714        match self.advance() {
6715            Token::Ident(s)
6716                if s.eq_ignore_ascii_case("deferred") || s.eq_ignore_ascii_case("immediate") =>
6717            {
6718                Ok(s.eq_ignore_ascii_case("deferred"))
6719            }
6720            other => Err(self.err(alloc::format!(
6721                "expected DEFERRED or IMMEDIATE after INITIALLY, got {other:?}"
6722            ))),
6723        }
6724    }
6725
6726    /// v7.17.0 Phase 4.2 — consume a MySQL `CREATE PROCEDURE` body
6727    /// in its entirety so the parser returns Empty without
6728    /// touching the runtime. The CREATE+PROCEDURE keywords are
6729    /// already consumed; this swallows everything from the
6730    /// procedure name through the matching `END`, including
6731    /// nested `BEGIN`/`END` blocks, internal `;` terminators
6732    /// (DELIMITER `//` makes the script splitter forward the
6733    /// whole block as one statement), `@var` session-variable
6734    /// references, and the trailing terminator.
6735    ///
6736    /// Tracks nesting depth so:
6737    ///   BEGIN
6738    ///     IF cond THEN
6739    ///       BEGIN ... END;
6740    ///     END IF;
6741    ///   END
6742    /// terminates at the outer END.
6743    fn consume_mysql_routine_body(&mut self) {
6744        // Outer skeleton: name, (...), optional clauses, BEGIN
6745        // <body> END [;]. Scan for the first BEGIN — anything
6746        // before it is signature decoration we don't care about.
6747        // Once inside BEGIN, count up on BEGIN, down on END.
6748        let mut depth: i32 = 0;
6749        let mut started = false;
6750        loop {
6751            match self.peek().clone() {
6752                Token::Begin => {
6753                    self.advance();
6754                    depth += 1;
6755                    started = true;
6756                }
6757                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
6758                    self.advance();
6759                    if started {
6760                        depth -= 1;
6761                        if depth <= 0 {
6762                            // Optional trailing ident (`END IF`,
6763                            // `END LOOP`, `END WHILE`, `END CASE`,
6764                            // `END label_name`) — eat the next
6765                            // ident if present so we don't
6766                            // mistake `END IF;` for the outer
6767                            // close.
6768                            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6769                                // If the next token is one of the
6770                                // PL/SQL block-closer keywords,
6771                                // the END belongs to an inner
6772                                // block; bump depth back up.
6773                                let is_inner_close = matches!(
6774                                    self.peek(),
6775                                    Token::Ident(s) | Token::QuotedIdent(s)
6776                                        if matches!(
6777                                            s.to_ascii_lowercase().as_str(),
6778                                            "if" | "loop" | "while" | "case" | "repeat"
6779                                        )
6780                                );
6781                                if is_inner_close {
6782                                    self.advance();
6783                                    depth += 1;
6784                                    continue;
6785                                }
6786                            }
6787                            // Eat optional trailing `;`.
6788                            if matches!(self.peek(), Token::Semicolon) {
6789                                self.advance();
6790                            }
6791                            return;
6792                        }
6793                    }
6794                }
6795                Token::Eof => return,
6796                _ => {
6797                    self.advance();
6798                }
6799            }
6800        }
6801    }
6802
6803    /// v7.17.0 Phase 2.6 — absorb the MySQL view-prefix clauses
6804    /// that appear between `CREATE` and `VIEW` in mysqldump output:
6805    ///
6806    /// * `ALGORITHM = {UNDEFINED|MERGE|TEMPTABLE}`
6807    /// * `DEFINER = <user>`  (user may be a quoted string, a bare
6808    ///   ident, or `ident @ ident-or-quoted-string` host form)
6809    /// * `SQL SECURITY {DEFINER|INVOKER}`
6810    ///
6811    /// Each clause may appear at most once but in any order.
6812    /// The hints are pure planner / permission metadata that
6813    /// SPG's view-rewrite engine handles uniformly; we accept
6814    /// and discard. Returns `Ok(())` once a non-clause token is
6815    /// peeked (the caller then checks for the `VIEW` keyword).
6816    fn consume_mysql_view_prefix(&mut self) -> Result<(), ParseError> {
6817        loop {
6818            match self.peek().clone() {
6819                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("algorithm") => {
6820                    self.advance(); // ALGORITHM
6821                    // Optional `=`. MySQL spec requires it but be
6822                    // generous.
6823                    if matches!(self.peek(), Token::Eq) {
6824                        self.advance();
6825                    }
6826                    // UNDEFINED / MERGE / TEMPTABLE — accept any
6827                    // bare ident; unknown values still parse so
6828                    // future MySQL versions don't break.
6829                    if matches!(
6830                        self.peek(),
6831                        Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6832                    ) {
6833                        self.advance();
6834                    }
6835                }
6836                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("definer") => {
6837                    self.advance(); // DEFINER
6838                    if matches!(self.peek(), Token::Eq) {
6839                        self.advance();
6840                    }
6841                    // User: quoted string, ident, OR ident @ host
6842                    // (host may itself be quoted or bare).
6843                    match self.peek().clone() {
6844                        Token::String(_) | Token::Ident(_) | Token::QuotedIdent(_) => {
6845                            self.advance();
6846                            // Optional `@host`.
6847                            if matches!(self.peek(), Token::At) {
6848                                self.advance();
6849                                if matches!(
6850                                    self.peek(),
6851                                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
6852                                ) {
6853                                    self.advance();
6854                                }
6855                            }
6856                        }
6857                        _ => {}
6858                    }
6859                }
6860                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sql") => {
6861                    // `SQL SECURITY {DEFINER|INVOKER}`. Only honoured
6862                    // when followed by SECURITY — the dispatcher must
6863                    // not consume a bare `SQL` token (it's not a
6864                    // legal CREATE prefix on its own).
6865                    let save = self.pos;
6866                    self.advance(); // SQL
6867                    if matches!(self.peek(), Token::Ident(s2) | Token::QuotedIdent(s2)
6868                        if s2.eq_ignore_ascii_case("security"))
6869                    {
6870                        self.advance(); // SECURITY
6871                        // DEFINER / INVOKER trailing ident.
6872                        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
6873                            self.advance();
6874                        }
6875                    } else {
6876                        // Not a SQL SECURITY clause — roll back and
6877                        // bail; the caller will error out cleanly.
6878                        self.pos = save;
6879                        return Ok(());
6880                    }
6881                }
6882                _ => return Ok(()),
6883            }
6884        }
6885    }
6886
6887    fn parse_if_not_exists(&mut self) -> bool {
6888        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6889        {
6890            let save = self.pos;
6891            self.advance();
6892            if matches!(self.peek(), Token::Not) {
6893                self.advance();
6894                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6895                {
6896                    self.advance();
6897                    return true;
6898                }
6899            }
6900            self.pos = save;
6901        }
6902        false
6903    }
6904
6905    fn parse_if_exists(&mut self) -> bool {
6906        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
6907        {
6908            let save = self.pos;
6909            self.advance();
6910            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists"))
6911            {
6912                self.advance();
6913                return true;
6914            }
6915            self.pos = save;
6916        }
6917        false
6918    }
6919
6920    /// v7.12.4 — body of `CREATE [OR REPLACE] TRIGGER`. The
6921    /// `[OR REPLACE]` flag and the `TRIGGER` keyword have already
6922    /// been consumed.
6923    fn parse_create_trigger_after_keyword(
6924        &mut self,
6925        or_replace: bool,
6926    ) -> Result<Statement, ParseError> {
6927        let name = self.expect_ident_like()?;
6928        let timing = {
6929            let ident = self.expect_ident_like()?;
6930            if ident.eq_ignore_ascii_case("before") {
6931                TriggerTiming::Before
6932            } else if ident.eq_ignore_ascii_case("after") {
6933                TriggerTiming::After
6934            } else if ident.eq_ignore_ascii_case("instead") {
6935                let next = self.expect_ident_like()?;
6936                if !next.eq_ignore_ascii_case("of") {
6937                    return Err(self.err(alloc::format!(
6938                        "expected OF after INSTEAD in trigger timing, got {next:?}"
6939                    )));
6940                }
6941                TriggerTiming::InsteadOf
6942            } else {
6943                return Err(self.err(alloc::format!(
6944                    "expected BEFORE / AFTER / INSTEAD OF in trigger timing, got {ident:?}"
6945                )));
6946            }
6947        };
6948        // Events: INSERT [ OR UPDATE [ OR DELETE [ OR TRUNCATE ] ] ].
6949        // OR is a reserved keyword token (Token::Or), not an Ident.
6950        // v7.13.0 — after an UPDATE event we may optionally see
6951        // `OF col, col, …` (mailrs round-5 G7). Columns are
6952        // captured into `update_columns` once across the whole
6953        // events list; multiple `UPDATE OF` clauses are rejected.
6954        let mut events: Vec<TriggerEvent> = Vec::new();
6955        let mut update_columns: Vec<String> = Vec::new();
6956        let (first_ev, first_cols) = self.parse_trigger_event_with_optional_of()?;
6957        events.push(first_ev);
6958        if !first_cols.is_empty() {
6959            update_columns = first_cols;
6960        }
6961        while matches!(self.peek(), Token::Or) {
6962            self.advance();
6963            let (ev, cols) = self.parse_trigger_event_with_optional_of()?;
6964            events.push(ev);
6965            if !cols.is_empty() {
6966                if !update_columns.is_empty() {
6967                    return Err(
6968                        self.err("CREATE TRIGGER: `UPDATE OF cols` may appear at most once".into())
6969                    );
6970                }
6971                update_columns = cols;
6972            }
6973        }
6974        // ON <table>
6975        let tok = self.peek();
6976        let Token::On = tok else {
6977            return Err(self.err(alloc::format!(
6978                "expected ON after trigger events, got {tok:?}"
6979            )));
6980        };
6981        self.advance();
6982        let table = self.expect_ident_like()?;
6983        // v7.39 (read01 round 82) — a CONSTRAINT TRIGGER may carry `FROM
6984        // reftable` and `[NOT] DEFERRABLE [INITIALLY {DEFERRED|IMMEDIATE}]`
6985        // between the table and FOR EACH ROW. Accept and skip them: SPG fires
6986        // the trigger as a plain AFTER trigger (correct for every non-deferred
6987        // use; deferral timing is not yet honoured).
6988        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
6989            if s.eq_ignore_ascii_case("from"))
6990        {
6991            self.advance();
6992            let _reftable = self.expect_ident_like()?;
6993        }
6994        self.consume_optional_deferrable_clauses()?;
6995        // FOR EACH ROW / FOR EACH STATEMENT. FOR is a reserved
6996        // keyword (Token::For); EACH / ROW / STATEMENT are bare
6997        // idents.
6998        if !matches!(self.peek(), Token::For) {
6999            return Err(self.err(alloc::format!(
7000                "expected FOR EACH ROW / STATEMENT, got {:?}",
7001                self.peek()
7002            )));
7003        }
7004        self.advance();
7005        let for_each = {
7006            let e = self.expect_ident_like()?;
7007            if !e.eq_ignore_ascii_case("each") {
7008                return Err(self.err(alloc::format!("expected EACH after FOR, got {e:?}")));
7009            }
7010            let unit = self.expect_ident_like()?;
7011            if unit.eq_ignore_ascii_case("row") {
7012                TriggerForEach::Row
7013            } else if unit.eq_ignore_ascii_case("statement") {
7014                TriggerForEach::Statement
7015            } else {
7016                return Err(self.err(alloc::format!(
7017                    "expected ROW / STATEMENT after FOR EACH, got {unit:?}"
7018                )));
7019            }
7020        };
7021        // v7.39 (round 138) — optional `WHEN ( condition )` before EXECUTE.
7022        let when_condition = if matches!(self.peek(),
7023            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7024        {
7025            self.advance();
7026            Some(self.parse_paren_expr("WHEN")?)
7027        } else {
7028            None
7029        };
7030        // EXECUTE FUNCTION/PROCEDURE name(...)
7031        let exec = self.expect_ident_like()?;
7032        if !exec.eq_ignore_ascii_case("execute") {
7033            return Err(self.err(alloc::format!(
7034                "expected EXECUTE FUNCTION/PROCEDURE in CREATE TRIGGER, got {exec:?}"
7035            )));
7036        }
7037        let fn_or_proc = self.expect_ident_like()?;
7038        if !(fn_or_proc.eq_ignore_ascii_case("function")
7039            || fn_or_proc.eq_ignore_ascii_case("procedure"))
7040        {
7041            return Err(self.err(alloc::format!(
7042                "expected FUNCTION / PROCEDURE after EXECUTE, got {fn_or_proc:?}"
7043            )));
7044        }
7045        let function = self.expect_ident_like()?;
7046        // Optional empty arg list `()`.
7047        if matches!(self.peek(), Token::LParen) {
7048            self.advance();
7049            if !matches!(self.peek(), Token::RParen) {
7050                return Err(self.err(alloc::format!(
7051                    "v7.12.4 trigger function calls take no args; got {:?}",
7052                    self.peek()
7053                )));
7054            }
7055            self.advance();
7056        }
7057        Ok(Statement::CreateTrigger(CreateTriggerStatement {
7058            name,
7059            or_replace,
7060            timing,
7061            events,
7062            table,
7063            for_each,
7064            function,
7065            update_columns,
7066            when_condition,
7067        }))
7068    }
7069
7070    /// v7.39 (round 139) — `CREATE RULE <name> AS ON <event> TO <table>
7071    /// [WHERE <cond>] DO [ALSO|INSTEAD] { NOTHING | cmd | ( cmd; … ) }`.
7072    fn parse_create_rule_after_keyword(
7073        &mut self,
7074        or_replace: bool,
7075    ) -> Result<Statement, ParseError> {
7076        let name = self.expect_ident_like()?;
7077        if !matches!(self.peek(), Token::As) {
7078            return Err(self.err(alloc::format!(
7079                "expected AS in CREATE RULE, got {:?}",
7080                self.peek()
7081            )));
7082        }
7083        self.advance();
7084        if !matches!(self.peek(), Token::On) {
7085            return Err(self.err(alloc::format!(
7086                "expected ON in CREATE RULE, got {:?}",
7087                self.peek()
7088            )));
7089        }
7090        self.advance();
7091        let event = self.parse_rule_event()?;
7092        if !matches!(self.peek(), Token::To)
7093            && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("to"))
7094        {
7095            return Err(self.err(alloc::format!(
7096                "expected TO after rule event, got {:?}",
7097                self.peek()
7098            )));
7099        }
7100        self.advance();
7101        let table = self.expect_ident_like()?;
7102        // Optional `WHERE <cond>` (no parentheses, unlike a trigger WHEN).
7103        let when_condition = if matches!(self.peek(), Token::Where) {
7104            self.advance();
7105            Some(self.parse_expr(0)?)
7106        } else {
7107            None
7108        };
7109        if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do"))
7110        {
7111            return Err(self.err(alloc::format!(
7112                "expected DO in CREATE RULE, got {:?}",
7113                self.peek()
7114            )));
7115        }
7116        self.advance();
7117        // `DO [ ALSO | INSTEAD ]` — ALSO is the default when neither is written.
7118        let instead = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("instead"))
7119        {
7120            self.advance();
7121            true
7122        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("also")) {
7123            self.advance();
7124            false
7125        } else {
7126            false
7127        };
7128        // `NOTHING` | `( cmd; … )` | `cmd`.
7129        let commands = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nothing"))
7130        {
7131            self.advance();
7132            Vec::new()
7133        } else if matches!(self.peek(), Token::LParen) {
7134            self.advance();
7135            let mut cmds = Vec::new();
7136            loop {
7137                cmds.push(self.parse_one_statement()?);
7138                if matches!(self.peek(), Token::Semicolon) {
7139                    self.advance();
7140                    if matches!(self.peek(), Token::RParen) {
7141                        break;
7142                    }
7143                    continue;
7144                }
7145                break;
7146            }
7147            if !matches!(self.peek(), Token::RParen) {
7148                return Err(self.err(alloc::format!(
7149                    "expected ) closing the CREATE RULE command list, got {:?}",
7150                    self.peek()
7151                )));
7152            }
7153            self.advance();
7154            cmds
7155        } else {
7156            alloc::vec![self.parse_one_statement()?]
7157        };
7158        Ok(Statement::CreateRule(crate::ast::CreateRuleStatement {
7159            name,
7160            or_replace,
7161            event,
7162            table,
7163            instead,
7164            when_condition,
7165            commands,
7166        }))
7167    }
7168
7169    /// v7.39 (round 139) — a rule event keyword → uppercase string.
7170    fn parse_rule_event(&mut self) -> Result<alloc::string::String, ParseError> {
7171        if matches!(self.peek(), Token::Insert) {
7172            self.advance();
7173            return Ok(alloc::string::String::from("INSERT"));
7174        }
7175        if matches!(self.peek(), Token::Select) {
7176            self.advance();
7177            return Ok(alloc::string::String::from("SELECT"));
7178        }
7179        match self.peek() {
7180            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
7181                self.advance();
7182                Ok(alloc::string::String::from("UPDATE"))
7183            }
7184            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
7185                self.advance();
7186                Ok(alloc::string::String::from("DELETE"))
7187            }
7188            other => Err(self.err(alloc::format!(
7189                "expected INSERT / UPDATE / DELETE / SELECT in CREATE RULE, got {other:?}"
7190            ))),
7191        }
7192    }
7193
7194    /// v7.13.0 — parse one trigger event, then optionally consume
7195    /// `OF col, col, …` after `UPDATE` (mailrs round-5 G7). Other
7196    /// events (INSERT/DELETE/TRUNCATE) don't accept the OF tail.
7197    fn parse_trigger_event_with_optional_of(
7198        &mut self,
7199    ) -> Result<(TriggerEvent, Vec<String>), ParseError> {
7200        let ev = self.parse_trigger_event()?;
7201        if !matches!(ev, TriggerEvent::Update) {
7202            return Ok((ev, Vec::new()));
7203        }
7204        // `OF` is a bare ident.
7205        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("of")) {
7206            return Ok((ev, Vec::new()));
7207        }
7208        self.advance(); // OF
7209        let mut cols: Vec<String> = Vec::new();
7210        loop {
7211            cols.push(self.expect_ident_like()?);
7212            if matches!(self.peek(), Token::Comma) {
7213                self.advance();
7214                continue;
7215            }
7216            break;
7217        }
7218        if cols.is_empty() {
7219            return Err(
7220                self.err("CREATE TRIGGER: `UPDATE OF` requires at least one column name".into())
7221            );
7222        }
7223        Ok((ev, cols))
7224    }
7225
7226    /// v7.12.4 — `BEGIN stmt; stmt; … END[;]` PL/pgSQL block.
7227    /// v7.12.6 — optional `DECLARE var TYPE [:= init];` prelude
7228    /// before `BEGIN`, and IF / RAISE / embedded SQL statements
7229    /// inside the body.
7230    /// Called by [`parse_plpgsql_body`] after the body's tokens
7231    /// have been lexed into this temporary parser.
7232    pub(crate) fn parse_plpgsql_block(&mut self) -> Result<PlPgSqlBlock, ParseError> {
7233        // v7.12.6 — optional DECLARE prelude.
7234        let declarations = if matches!(
7235            self.peek(),
7236            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("declare")
7237        ) {
7238            self.advance();
7239            self.parse_plpgsql_declare_block()?
7240        } else {
7241            Vec::new()
7242        };
7243        // BEGIN keyword (PL/pgSQL — distinct from the SQL
7244        // `BEGIN` transaction-start, but we can reuse the
7245        // reserved Token::Begin since the body is a separate
7246        // lex/parse context).
7247        if !matches!(self.peek(), Token::Begin) {
7248            return Err(self.err(alloc::format!(
7249                "expected BEGIN at start of plpgsql block, got {:?}",
7250                self.peek()
7251            )));
7252        }
7253        self.advance();
7254        let statements = self.parse_plpgsql_stmt_list_until_end()?;
7255        // v7.37.20 (20.10) — optional EXCEPTION clause between the
7256        // body's last statement and the trailing END. When present
7257        // it's a series of `WHEN <cond> [OR <cond>]* THEN <body>`
7258        // arms terminated by END.
7259        let exception_handlers = if matches!(
7260            self.peek(),
7261            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exception")
7262        ) {
7263            self.advance();
7264            self.parse_plpgsql_exception_handlers()?
7265        } else {
7266            Vec::new()
7267        };
7268        Ok(PlPgSqlBlock {
7269            declarations,
7270            statements,
7271            exception_handlers,
7272        })
7273    }
7274
7275    /// v7.37.20 (20.10) — parse EXCEPTION handlers `WHEN <cond>
7276    /// [OR <cond>]* THEN <body>` sequence up to the trailing END.
7277    fn parse_plpgsql_exception_handlers(
7278        &mut self,
7279    ) -> Result<Vec<crate::ast::ExceptionHandler>, ParseError> {
7280        let mut out: Vec<crate::ast::ExceptionHandler> = Vec::new();
7281        loop {
7282            // Stop at END — the block-level trailing END LOOP / END;
7283            // is handled by the caller.
7284            if matches!(
7285                self.peek(),
7286                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end")
7287            ) {
7288                return Ok(out);
7289            }
7290            // WHEN <cond> [OR <cond>]* THEN <body>
7291            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7292            {
7293                return Err(self.err(alloc::format!(
7294                    "expected WHEN or END inside EXCEPTION clause, got {:?}",
7295                    self.peek()
7296                )));
7297            }
7298            self.advance();
7299            let mut conditions: Vec<String> = Vec::new();
7300            conditions.push(self.expect_ident_like()?);
7301            while matches!(self.peek(), Token::Or) {
7302                self.advance();
7303                conditions.push(self.expect_ident_like()?);
7304            }
7305            let then_kw = self.expect_ident_like()?;
7306            if !then_kw.eq_ignore_ascii_case("then") {
7307                return Err(self.err(alloc::format!(
7308                    "expected THEN after WHEN condition list, got {then_kw:?}"
7309                )));
7310            }
7311            let body = self.parse_plpgsql_stmt_list_until_end()?;
7312            out.push(crate::ast::ExceptionHandler { conditions, body });
7313        }
7314    }
7315
7316    /// v7.12.6 — parse the `DECLARE ... [var TYPE [:= init];]+`
7317    /// prelude. Caller has already consumed `DECLARE`. We stop
7318    /// reading entries when we hit `BEGIN`.
7319    fn parse_plpgsql_declare_block(&mut self) -> Result<Vec<PlPgSqlDeclare>, ParseError> {
7320        let mut out: Vec<PlPgSqlDeclare> = Vec::new();
7321        loop {
7322            if matches!(self.peek(), Token::Begin) {
7323                return Ok(out);
7324            }
7325            let name = self.expect_ident_like()?;
7326            // v7.37.20 (20.7) — type inference: if the next token is
7327            // `:=` or `=` (no explicit type), infer from the default
7328            // expression. Otherwise the ident that follows is the
7329            // declared type.
7330            //
7331            // v7.37.20 (20.8) — `<table>.<col>%TYPE` / `<table>%ROWTYPE`
7332            // (PG-standard). SPG parse-accepts and treats identically
7333            // to inference — the eventual runtime value determines
7334            // the local's type, which is faithful to how SPG handles
7335            // untyped locals today (see 20.7). Full compile-time
7336            // catalog lookup queues with v7.40 PL/pgSQL epic.
7337            let ty = if matches!(self.peek(), Token::ColonEq | Token::Eq) {
7338                // Sentinel: `FunctionArgType::Raw("_infer_")` tells the
7339                // downstream declaration walker to type the local by
7340                // the runtime type of the default expression.
7341                FunctionArgType::Raw("_infer_".into())
7342            } else {
7343                let ty_token = self.expect_ident_like()?;
7344                // Detect `<ident>[.<ident>][%TYPE | %ROWTYPE]`:
7345                // consume optional `.<ident>` qualifier + `%<KW>`
7346                // suffix. Both qualifier and suffix map to _infer_.
7347                if matches!(self.peek(), Token::Dot) {
7348                    self.advance();
7349                    let _ = self.expect_ident_like()?;
7350                }
7351                if matches!(self.peek(), Token::Percent) {
7352                    self.advance();
7353                    // Consume the trailing TYPE / ROWTYPE ident.
7354                    let _ = self.expect_ident_like()?;
7355                    FunctionArgType::Raw("_infer_".into())
7356                } else {
7357                    match map_type_ident_to_column_type_name(&ty_token) {
7358                        Some(t) => FunctionArgType::Typed(t),
7359                        None => FunctionArgType::Raw(ty_token),
7360                    }
7361                }
7362            };
7363            let default = match self.peek() {
7364                Token::ColonEq => {
7365                    self.advance();
7366                    Some(self.parse_expr(0)?)
7367                }
7368                Token::Eq => {
7369                    // PL/pgSQL also accepts `=` for the
7370                    // DECLARE default (PG treats them the same
7371                    // in this position).
7372                    self.advance();
7373                    Some(self.parse_expr(0)?)
7374                }
7375                _ => None,
7376            };
7377            // Mandatory `;` between declarations.
7378            if !matches!(self.peek(), Token::Semicolon) {
7379                return Err(self.err(alloc::format!(
7380                    "expected ; after DECLARE entry for {name:?}, got {:?}",
7381                    self.peek()
7382                )));
7383            }
7384            self.advance();
7385            out.push(PlPgSqlDeclare { name, ty, default });
7386        }
7387    }
7388
7389    /// v7.12.6 — parse PL/pgSQL statements up to (and consuming)
7390    /// the terminating `END;` (or `END IF;` etc — handled by the
7391    /// per-construct sub-parsers). Used by both the outer block
7392    /// and the IF/ELSE branch bodies.
7393    fn parse_plpgsql_stmt_list_until_end(&mut self) -> Result<Vec<PlPgSqlStmt>, ParseError> {
7394        let mut statements: Vec<PlPgSqlStmt> = Vec::new();
7395        loop {
7396            // Allow trailing semicolons + END.
7397            while matches!(self.peek(), Token::Semicolon) {
7398                self.advance();
7399            }
7400            // END / ELSE / ELSIF / EXCEPTION — handled by the caller.
7401            if matches!(
7402                self.peek(),
7403                Token::Ident(s) | Token::QuotedIdent(s)
7404                    if s.eq_ignore_ascii_case("end")
7405                        || s.eq_ignore_ascii_case("else")
7406                        || s.eq_ignore_ascii_case("elsif")
7407                        || s.eq_ignore_ascii_case("elseif")
7408                        || s.eq_ignore_ascii_case("exception")
7409                        || s.eq_ignore_ascii_case("when")
7410            ) {
7411                return Ok(statements);
7412            }
7413            // Otherwise: one statement, then expect `;` or
7414            // a block-terminator keyword.
7415            let stmt = self.parse_plpgsql_stmt()?;
7416            statements.push(stmt);
7417            match self.peek() {
7418                Token::Semicolon => {
7419                    self.advance();
7420                }
7421                Token::Ident(s) | Token::QuotedIdent(s)
7422                    if s.eq_ignore_ascii_case("end")
7423                        || s.eq_ignore_ascii_case("else")
7424                        || s.eq_ignore_ascii_case("elsif")
7425                        || s.eq_ignore_ascii_case("elseif")
7426                        || s.eq_ignore_ascii_case("exception")
7427                        || s.eq_ignore_ascii_case("when") =>
7428                {
7429                    // Final statement of the block without `;`.
7430                }
7431                other => {
7432                    return Err(self.err(alloc::format!(
7433                        "expected ; or END/ELSE/ELSIF after plpgsql statement, got {other:?}"
7434                    )));
7435                }
7436            }
7437        }
7438    }
7439
7440    fn parse_plpgsql_stmt(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7441        // RETURN keyword?
7442        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("return"))
7443        {
7444            self.advance();
7445            return self.parse_plpgsql_return();
7446        }
7447        // v7.12.6 — IF block.
7448        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("if"))
7449        {
7450            self.advance();
7451            return self.parse_plpgsql_if();
7452        }
7453        // v7.37.20 (20.6) — FOR <var> IN EXECUTE <string_expr> LOOP.
7454        // Detected by peeking that token pos+3 is Ident("execute").
7455        if matches!(self.peek(), Token::For)
7456            && matches!(
7457                self.tokens.get(self.pos + 1),
7458                Some(Token::Ident(_) | Token::QuotedIdent(_))
7459            )
7460            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7461            && matches!(
7462                self.tokens.get(self.pos + 3),
7463                Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("execute")
7464            )
7465        {
7466            self.advance(); // FOR
7467            let var = self.expect_ident_like()?;
7468            self.advance(); // IN
7469            self.advance(); // EXECUTE
7470            // Prescan for LOOP at paren depth 0 so parse_expr stops
7471            // before the LOOP keyword (same trick as the bare-SELECT
7472            // ForQuery arm).
7473            let mut depth: i32 = 0;
7474            let mut loop_pos: Option<usize> = None;
7475            let mut scan = self.pos;
7476            while scan < self.tokens.len() {
7477                match self.tokens.get(scan) {
7478                    Some(Token::LParen) => depth += 1,
7479                    Some(Token::RParen) => depth -= 1,
7480                    Some(Token::Ident(s) | Token::QuotedIdent(s))
7481                        if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7482                    {
7483                        loop_pos = Some(scan);
7484                        break;
7485                    }
7486                    _ => {}
7487                }
7488                scan += 1;
7489            }
7490            let loop_pos = loop_pos.ok_or_else(|| {
7491                self.err(alloc::format!(
7492                    "FOR <var> IN EXECUTE <expr> ... LOOP: no LOOP keyword found"
7493                ))
7494            })?;
7495            let saved_loop = self.tokens[loop_pos].clone();
7496            self.tokens[loop_pos] = Token::Semicolon;
7497            let expr_result = self.parse_expr(0);
7498            self.tokens[loop_pos] = saved_loop;
7499            let sql_expr = expr_result?;
7500            let loop_kw = self.expect_ident_like()?;
7501            if !loop_kw.eq_ignore_ascii_case("loop") {
7502                return Err(self.err(alloc::format!(
7503                    "expected LOOP after FOR <var> IN EXECUTE <expr>, got {loop_kw:?}"
7504                )));
7505            }
7506            let body = self.parse_plpgsql_stmt_list_until_end()?;
7507            let end_kw = self.expect_ident_like()?;
7508            if !end_kw.eq_ignore_ascii_case("end") {
7509                return Err(self.err(alloc::format!(
7510                    "expected END LOOP after FOR IN EXECUTE body, got {end_kw:?}"
7511                )));
7512            }
7513            let loop_kw2 = self.expect_ident_like()?;
7514            if !loop_kw2.eq_ignore_ascii_case("loop") {
7515                return Err(self.err(alloc::format!(
7516                    "expected END LOOP after FOR IN EXECUTE body, got END {loop_kw2:?}"
7517                )));
7518            }
7519            return Ok(PlPgSqlStmt::ForExecute {
7520                var,
7521                sql_expr,
7522                body,
7523            });
7524        }
7525        // v7.37.20 (20.5) — FOR <var> IN <SELECT> LOOP.
7526        //
7527        // Two syntactic forms:
7528        //   FOR var IN SELECT ... ORDER BY ... LOOP ...
7529        //   FOR var IN (SELECT ...) LOOP ...
7530        //
7531        // Bare-SELECT form: to keep parse_select_stmt from swallowing
7532        // the trailing `LOOP` keyword as a table alias, we prescan
7533        // forward to find LOOP at paren depth 0, splice a fake
7534        // Semicolon at that position (so SELECT parses cleanly),
7535        // then re-splice LOOP back in.
7536        //
7537        // Paren-wrapped form: parse `(` `SELECT ...` `)` then expect
7538        // LOOP directly — no scan required.
7539        if matches!(self.peek(), Token::For)
7540            && matches!(
7541                self.tokens.get(self.pos + 1),
7542                Some(Token::Ident(_) | Token::QuotedIdent(_))
7543            )
7544            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7545            && (matches!(self.tokens.get(self.pos + 3), Some(Token::Select))
7546                || matches!(self.tokens.get(self.pos + 3), Some(Token::LParen)))
7547        {
7548            self.advance(); // FOR
7549            let var = self.expect_ident_like()?;
7550            // IN
7551            self.advance();
7552            let query = if matches!(self.peek(), Token::LParen) {
7553                // Paren-wrapped SELECT.
7554                self.advance();
7555                let inner = self.parse_select_stmt()?;
7556                let Statement::Select(q) = inner else {
7557                    return Err(self.err(alloc::format!(
7558                        "expected SELECT inside (…), got {:?}",
7559                        self.peek()
7560                    )));
7561                };
7562                if !matches!(self.peek(), Token::RParen) {
7563                    return Err(self.err(alloc::format!(
7564                        "expected ')' after FOR-IN-SELECT body, got {:?}",
7565                        self.peek()
7566                    )));
7567                }
7568                self.advance();
7569                q
7570            } else {
7571                // Bare SELECT: prescan to find the LOOP boundary.
7572                let mut depth: i32 = 0;
7573                let mut loop_pos: Option<usize> = None;
7574                let mut scan = self.pos;
7575                while scan < self.tokens.len() {
7576                    match self.tokens.get(scan) {
7577                        Some(Token::LParen) => depth += 1,
7578                        Some(Token::RParen) => depth -= 1,
7579                        Some(Token::Ident(s) | Token::QuotedIdent(s))
7580                            if depth == 0 && s.eq_ignore_ascii_case("loop") =>
7581                        {
7582                            loop_pos = Some(scan);
7583                            break;
7584                        }
7585                        _ => {}
7586                    }
7587                    scan += 1;
7588                }
7589                let loop_pos = loop_pos.ok_or_else(|| {
7590                    self.err(alloc::format!(
7591                        "FOR <var> IN <SELECT> ... LOOP: no LOOP keyword found"
7592                    ))
7593                })?;
7594                // Swap the LOOP token with a synthetic Semicolon so
7595                // parse_select_stmt stops there, then restore afterward.
7596                let saved_loop = self.tokens[loop_pos].clone();
7597                self.tokens[loop_pos] = Token::Semicolon;
7598                let parse_result = self.parse_select_stmt();
7599                self.tokens[loop_pos] = saved_loop;
7600                let inner = parse_result?;
7601                let Statement::Select(q) = inner else {
7602                    return Err(self.err(alloc::format!(
7603                        "expected SELECT after FOR <var> IN, got {:?}",
7604                        self.peek()
7605                    )));
7606                };
7607                q
7608            };
7609            let loop_kw = self.expect_ident_like()?;
7610            if !loop_kw.eq_ignore_ascii_case("loop") {
7611                return Err(self.err(alloc::format!(
7612                    "expected LOOP after FOR <var> IN <SELECT>, got {loop_kw:?}"
7613                )));
7614            }
7615            let body = self.parse_plpgsql_stmt_list_until_end()?;
7616            let end_kw = self.expect_ident_like()?;
7617            if !end_kw.eq_ignore_ascii_case("end") {
7618                return Err(self.err(alloc::format!(
7619                    "expected END LOOP after FOR IN SELECT body, got {end_kw:?}"
7620                )));
7621            }
7622            let loop_kw2 = self.expect_ident_like()?;
7623            if !loop_kw2.eq_ignore_ascii_case("loop") {
7624                return Err(self.err(alloc::format!(
7625                    "expected END LOOP after FOR IN SELECT body, got END {loop_kw2:?}"
7626                )));
7627            }
7628            return Ok(PlPgSqlStmt::ForQuery {
7629                var,
7630                query: Box::new(query),
7631                body,
7632            });
7633        }
7634        // v7.37.20 (20.4) — FOR <var> IN [REVERSE] <start>..<end> LOOP.
7635        // FOR is a reserved keyword token (Token::For).
7636        if matches!(self.peek(), Token::For)
7637            && matches!(
7638                self.tokens.get(self.pos + 1),
7639                Some(Token::Ident(_) | Token::QuotedIdent(_))
7640            )
7641            && matches!(self.tokens.get(self.pos + 2), Some(Token::In))
7642        {
7643            self.advance(); // FOR
7644            let var = self.expect_ident_like()?;
7645            if !matches!(self.peek(), Token::In) {
7646                return Err(self.err(alloc::format!(
7647                    "expected IN after FOR <var>, got {:?}",
7648                    self.peek()
7649                )));
7650            }
7651            self.advance();
7652            let reverse = matches!(
7653                self.peek(),
7654                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("reverse")
7655            );
7656            if reverse {
7657                self.advance();
7658            }
7659            let start = self.parse_expr(0)?;
7660            if !matches!(self.peek(), Token::DotDot) {
7661                return Err(self.err(alloc::format!(
7662                    "expected '..' between FOR loop bounds, got {:?}",
7663                    self.peek()
7664                )));
7665            }
7666            self.advance();
7667            let end = self.parse_expr(0)?;
7668            let loop_kw = self.expect_ident_like()?;
7669            if !loop_kw.eq_ignore_ascii_case("loop") {
7670                return Err(self.err(alloc::format!(
7671                    "expected LOOP after FOR <var> IN start..end, got {loop_kw:?}"
7672                )));
7673            }
7674            let body = self.parse_plpgsql_stmt_list_until_end()?;
7675            let end_kw = self.expect_ident_like()?;
7676            if !end_kw.eq_ignore_ascii_case("end") {
7677                return Err(self.err(alloc::format!(
7678                    "expected END LOOP after FOR body, got {end_kw:?}"
7679                )));
7680            }
7681            let loop_kw2 = self.expect_ident_like()?;
7682            if !loop_kw2.eq_ignore_ascii_case("loop") {
7683                return Err(self.err(alloc::format!(
7684                    "expected END LOOP after FOR body, got END {loop_kw2:?}"
7685                )));
7686            }
7687            return Ok(PlPgSqlStmt::ForRange {
7688                var,
7689                start,
7690                end,
7691                reverse,
7692                body,
7693            });
7694        }
7695        // v7.37.20 (20.2) — bare `LOOP <body> END LOOP;`.
7696        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("loop"))
7697        {
7698            self.advance();
7699            let body = self.parse_plpgsql_stmt_list_until_end()?;
7700            let end_kw = self.expect_ident_like()?;
7701            if !end_kw.eq_ignore_ascii_case("end") {
7702                return Err(self.err(alloc::format!(
7703                    "expected END LOOP after LOOP body, got {end_kw:?}"
7704                )));
7705            }
7706            let loop_kw = self.expect_ident_like()?;
7707            if !loop_kw.eq_ignore_ascii_case("loop") {
7708                return Err(self.err(alloc::format!(
7709                    "expected END LOOP after LOOP body, got END {loop_kw:?}"
7710                )));
7711            }
7712            return Ok(PlPgSqlStmt::Loop { body });
7713        }
7714        // v7.37.20 (20.2) — `EXIT [WHEN <cond>]` inside a loop.
7715        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exit"))
7716        {
7717            self.advance();
7718            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7719            {
7720                self.advance();
7721                Some(self.parse_expr(0)?)
7722            } else {
7723                None
7724            };
7725            return Ok(PlPgSqlStmt::Exit { when });
7726        }
7727        // v7.37.20 (20.13) — `EXECUTE <string_expr>`. Dispatches an
7728        // already-parsed Statement or a runtime-computed SQL string.
7729        // The disambiguator vs the extended-query-protocol `EXECUTE
7730        // <stmt_name>` (which is a top-level Statement, not a
7731        // plpgsql line) is that inside a DO block / trigger body the
7732        // EXECUTE keyword ALWAYS refers to dynamic SQL.
7733        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
7734        {
7735            self.advance();
7736            let sql = self.parse_expr(0)?;
7737            return Ok(PlPgSqlStmt::ExecuteDynamic { sql });
7738        }
7739        // v7.37.20 (20.2) — `CONTINUE [WHEN <cond>]` inside a loop.
7740        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("continue"))
7741        {
7742            self.advance();
7743            let when = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when"))
7744            {
7745                self.advance();
7746                Some(self.parse_expr(0)?)
7747            } else {
7748                None
7749            };
7750            return Ok(PlPgSqlStmt::Continue { when });
7751        }
7752        // v7.37.20 (20.3) — WHILE <cond> LOOP <body> END LOOP.
7753        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("while"))
7754        {
7755            self.advance();
7756            let condition = self.parse_expr(0)?;
7757            let loop_kw = self.expect_ident_like()?;
7758            if !loop_kw.eq_ignore_ascii_case("loop") {
7759                return Err(self.err(alloc::format!(
7760                    "expected LOOP after WHILE <condition>, got {loop_kw:?}"
7761                )));
7762            }
7763            let body = self.parse_plpgsql_stmt_list_until_end()?;
7764            // Expect END LOOP.
7765            let end_kw = self.expect_ident_like()?;
7766            if !end_kw.eq_ignore_ascii_case("end") {
7767                return Err(self.err(alloc::format!(
7768                    "expected END LOOP after WHILE body, got {end_kw:?}"
7769                )));
7770            }
7771            let loop_kw2 = self.expect_ident_like()?;
7772            if !loop_kw2.eq_ignore_ascii_case("loop") {
7773                return Err(self.err(alloc::format!(
7774                    "expected END LOOP after WHILE body, got END {loop_kw2:?}"
7775                )));
7776            }
7777            return Ok(PlPgSqlStmt::While { condition, body });
7778        }
7779        // v7.12.6 — RAISE.
7780        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("raise"))
7781        {
7782            self.advance();
7783            return self.parse_plpgsql_raise();
7784        }
7785        // v7.37.20 (20.14) — ASSERT <cond> [, <msg>].
7786        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("assert"))
7787        {
7788            self.advance();
7789            let condition = self.parse_expr(0)?;
7790            let message = if matches!(self.peek(), Token::Comma) {
7791                self.advance();
7792                Some(self.parse_expr(0)?)
7793            } else {
7794                None
7795            };
7796            return Ok(PlPgSqlStmt::Assert { condition, message });
7797        }
7798        // v7.37.20 (20.12) — PERFORM <select>. Per PG docs:
7799        //   "PERFORM is equivalent to SELECT but discards the
7800        //    result." Side effects (function calls, RAISE inside
7801        //    SQL functions, etc.) still execute. We desugar to
7802        //    `SELECT <body>` and wrap in EmbeddedSql so the engine's
7803        //    existing embedded-statement path handles execution +
7804        //    result-discard cleanly. The result is naturally
7805        //    discarded because EmbeddedSql doesn't propagate row
7806        //    sets back to the plpgsql interpreter.
7807        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("perform"))
7808        {
7809            self.advance();
7810            // Splice a synthetic Token::Select into the stream at
7811            // the current position so parse_select_stmt parses the
7812            // remainder as a normal SELECT body. Token-stream
7813            // surgery mirrors the try_parse_plpgsql_select_into
7814            // pattern used for SELECT … INTO desugaring.
7815            self.tokens.insert(self.pos, Token::Select);
7816            let select = self.parse_select_stmt()?;
7817            let Statement::Select(s) = select else {
7818                return Err(self.err(alloc::format!(
7819                    "expected SELECT body after PERFORM, got {:?}",
7820                    self.peek()
7821                )));
7822            };
7823            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(Statement::Select(s))));
7824        }
7825        // v7.16.2 — `SELECT <projection> INTO <var> [FROM …]`
7826        // plpgsql-specific shape (mailrs round-10 migrate-042).
7827        // PG's SELECT INTO at top-level SQL would CREATE a new
7828        // table; inside plpgsql it ASSIGNS the query result to
7829        // a local variable. We detect the INTO at paren-depth
7830        // 0 between SELECT and the statement boundary; if
7831        // found, split the token stream into "pre-INTO
7832        // projection" + "var" + "post-INTO FROM/WHERE…" and
7833        // rebuild as a SelectInto with a regular SELECT body
7834        // (no INTO clause).
7835        if matches!(self.peek(), Token::Select)
7836            && let Some((select_body, var_name)) = self.try_parse_plpgsql_select_into()?
7837        {
7838            return Ok(PlPgSqlStmt::SelectInto {
7839                var: var_name,
7840                body: Box::new(select_body),
7841            });
7842        }
7843        // v7.12.6 — embedded SQL statements. INSERT/UPDATE/DELETE/
7844        // SELECT can appear directly inside a trigger body; we
7845        // recurse into the regular Statement parser, which will
7846        // stop at the trailing `;` (which our caller then
7847        // consumes).
7848        // v7.16.2 — top-level DO blocks (mailrs round-10 A.2)
7849        // also embed ALTER / CREATE / DROP statements; route
7850        // those through the same parser so the DO body parses
7851        // cleanly.
7852        if matches!(self.peek(), Token::Insert)
7853            || matches!(self.peek(), Token::Select)
7854            || matches!(self.peek(), Token::Create)
7855            || matches!(self.peek(), Token::Drop)
7856            || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
7857                if s.eq_ignore_ascii_case("update")
7858                    || s.eq_ignore_ascii_case("delete")
7859                    || s.eq_ignore_ascii_case("alter"))
7860        {
7861            let stmt = self.parse_one_statement()?;
7862            return Ok(PlPgSqlStmt::EmbeddedSql(Box::new(stmt)));
7863        }
7864        // Otherwise: assignment. `NEW.col` / `OLD.col` / `var`
7865        // followed by `:=` and an expression.
7866        let target = self.parse_plpgsql_assign_target()?;
7867        // PL/pgSQL assignment uses `:=`. The lexer represents
7868        // this as a colon followed by `=`; check both shapes.
7869        match self.peek() {
7870            Token::ColonEq => {
7871                self.advance();
7872            }
7873            Token::Colon => {
7874                self.advance();
7875                if !matches!(self.peek(), Token::Eq) {
7876                    return Err(self.err(alloc::format!(
7877                        "expected := after plpgsql assign target, got `:` then {:?}",
7878                        self.peek()
7879                    )));
7880                }
7881                self.advance();
7882            }
7883            other => {
7884                return Err(self.err(alloc::format!(
7885                    "expected := after plpgsql assign target, got {other:?}"
7886                )));
7887            }
7888        }
7889        let value = self.parse_expr(0)?;
7890        Ok(PlPgSqlStmt::Assign { target, value })
7891    }
7892
7893    /// v7.12.6 — `IF cond THEN body [ELSIF cond THEN body]*
7894    /// [ELSE body] END IF`. `IF` keyword already consumed.
7895    fn parse_plpgsql_if(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7896        let mut branches: Vec<(Expr, Vec<PlPgSqlStmt>)> = Vec::new();
7897        let mut else_branch: Vec<PlPgSqlStmt> = Vec::new();
7898        loop {
7899            // <expr> THEN
7900            let cond = self.parse_expr(0)?;
7901            let then_kw = self.expect_ident_like()?;
7902            if !then_kw.eq_ignore_ascii_case("then") {
7903                return Err(self.err(alloc::format!(
7904                    "expected THEN after IF/ELSIF condition, got {then_kw:?}"
7905                )));
7906            }
7907            let body = self.parse_plpgsql_stmt_list_until_end()?;
7908            branches.push((cond, body));
7909            // Look at terminator: ELSIF/ELSEIF, ELSE, or END IF.
7910            match self.peek() {
7911                Token::Ident(s) | Token::QuotedIdent(s)
7912                    if s.eq_ignore_ascii_case("elsif") || s.eq_ignore_ascii_case("elseif") =>
7913                {
7914                    self.advance();
7915                    continue;
7916                }
7917                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("else") => {
7918                    self.advance();
7919                    else_branch = self.parse_plpgsql_stmt_list_until_end()?;
7920                    break;
7921                }
7922                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("end") => {
7923                    break;
7924                }
7925                other => {
7926                    return Err(self.err(alloc::format!(
7927                        "expected ELSIF / ELSE / END after IF branch body, got {other:?}"
7928                    )));
7929                }
7930            }
7931        }
7932        // Expect `END IF` (the END keyword is the one we're
7933        // looking at right now).
7934        let end_kw = self.expect_ident_like()?;
7935        if !end_kw.eq_ignore_ascii_case("end") {
7936            return Err(self.err(alloc::format!("expected END IF, got {end_kw:?}")));
7937        }
7938        let if_kw = self.expect_ident_like()?;
7939        if !if_kw.eq_ignore_ascii_case("if") {
7940            return Err(self.err(alloc::format!("expected END IF, got END {if_kw:?}")));
7941        }
7942        Ok(PlPgSqlStmt::If {
7943            branches,
7944            else_branch,
7945        })
7946    }
7947
7948    /// v7.12.6 — `RAISE { NOTICE | WARNING | INFO | LOG | DEBUG
7949    /// | EXCEPTION } '<message>' [, args]*`. The `RAISE` keyword
7950    /// is already consumed.
7951    fn parse_plpgsql_raise(&mut self) -> Result<PlPgSqlStmt, ParseError> {
7952        let lvl_ident = self.expect_ident_like()?;
7953        let level = match lvl_ident.to_ascii_lowercase().as_str() {
7954            "notice" => RaiseLevel::Notice,
7955            "warning" => RaiseLevel::Warning,
7956            "info" => RaiseLevel::Info,
7957            "log" => RaiseLevel::Log,
7958            "debug" => RaiseLevel::Debug,
7959            "exception" => RaiseLevel::Exception,
7960            other => {
7961                return Err(self.err(alloc::format!(
7962                    "expected RAISE level (NOTICE/WARNING/INFO/LOG/DEBUG/EXCEPTION), got {other:?}"
7963                )));
7964            }
7965        };
7966        // Message: required for v7.12.6. PG accepts a bare
7967        // RAISE-rethrow form (no message), reserved for future
7968        // RAISE-no-args support.
7969        let Token::String(msg) = self.peek() else {
7970            return Err(self.err(alloc::format!(
7971                "expected RAISE message string, got {:?}",
7972                self.peek()
7973            )));
7974        };
7975        let message = msg.clone();
7976        self.advance();
7977        // Optional comma-separated args (PG `%` format substitution).
7978        let mut args: Vec<Expr> = Vec::new();
7979        while matches!(self.peek(), Token::Comma) {
7980            self.advance();
7981            args.push(self.parse_expr(0)?);
7982        }
7983        Ok(PlPgSqlStmt::Raise {
7984            level,
7985            message,
7986            args,
7987        })
7988    }
7989
7990    /// v7.16.2 — scan ahead for a plpgsql-flavoured `SELECT
7991    /// <projection> INTO <var> [FROM …]` (mailrs round-10
7992    /// migrate-042). Returns `(rebuilt_select_without_into,
7993    /// var_name)` when the pattern matches; `None` for
7994    /// regular SELECTs (those go through the embedded-SQL
7995    /// path). Token-stream surgery so the rebuilt SELECT
7996    /// parses through the regular `parse_select_stmt`.
7997    #[allow(clippy::too_many_lines)]
7998    fn try_parse_plpgsql_select_into(
7999        &mut self,
8000    ) -> Result<Option<(SelectStatement, String)>, ParseError> {
8001        // Scan forward from `self.pos + 1` (past Token::Select)
8002        // for Token::Into at paren-depth 0, stopping at the
8003        // first `;`, `END`, `ELSE`, `ELSIF` keyword that would
8004        // end the plpgsql statement.
8005        let start = self.pos;
8006        let mut into_pos: Option<usize> = None;
8007        let mut depth: i32 = 0;
8008        let mut i = start + 1;
8009        while i < self.tokens.len() {
8010            match &self.tokens[i] {
8011                Token::LParen => depth += 1,
8012                Token::RParen => depth -= 1,
8013                Token::Semicolon if depth == 0 => break,
8014                Token::Ident(s)
8015                    if depth == 0
8016                        && (s.eq_ignore_ascii_case("end")
8017                            || s.eq_ignore_ascii_case("else")
8018                            || s.eq_ignore_ascii_case("elsif")) =>
8019                {
8020                    break;
8021                }
8022                Token::Into if depth == 0 => {
8023                    into_pos = Some(i);
8024                    break;
8025                }
8026                _ => {}
8027            }
8028            i += 1;
8029        }
8030        let Some(into_at) = into_pos else {
8031            return Ok(None);
8032        };
8033        // The token immediately after INTO must be the target
8034        // var ident; anything else (e.g. INSERT INTO table)
8035        // ruled out by the depth-0 check above. Capture it.
8036        let var = match self.tokens.get(into_at + 1) {
8037            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
8038            other => {
8039                return Err(self.err(alloc::format!(
8040                    "expected variable name after SELECT … INTO, got {other:?}"
8041                )));
8042            }
8043        };
8044        // Find the end of the plpgsql SELECT INTO statement —
8045        // same boundary rules as the depth-0 scan above.
8046        let mut end = into_at + 2;
8047        let mut depth2: i32 = 0;
8048        while end < self.tokens.len() {
8049            match &self.tokens[end] {
8050                Token::LParen => depth2 += 1,
8051                Token::RParen => depth2 -= 1,
8052                Token::Semicolon if depth2 == 0 => break,
8053                Token::Ident(s)
8054                    if depth2 == 0
8055                        && (s.eq_ignore_ascii_case("end")
8056                            || s.eq_ignore_ascii_case("else")
8057                            || s.eq_ignore_ascii_case("elsif")) =>
8058                {
8059                    break;
8060                }
8061                _ => {}
8062            }
8063            end += 1;
8064        }
8065        // Rebuild a token stream that represents the SELECT
8066        // WITHOUT the INTO clause: [SELECT .. up-to-INTO] + [
8067        // post-var tokens up to statement end]. Run the
8068        // regular `parse_select_stmt` against it.
8069        let mut rebuilt: Vec<Token> = Vec::with_capacity(end - start);
8070        for j in start..into_at {
8071            rebuilt.push(self.tokens[j].clone());
8072        }
8073        for j in (into_at + 2)..end {
8074            rebuilt.push(self.tokens[j].clone());
8075        }
8076        rebuilt.push(Token::Eof);
8077        let saved_pos = self.pos;
8078        let saved_tokens = core::mem::replace(&mut self.tokens, rebuilt);
8079        self.pos = 0;
8080        // parse_select_stmt → parse_bare_select consumes Token::Select itself.
8081        if !matches!(self.peek(), Token::Select) {
8082            self.tokens = saved_tokens;
8083            self.pos = saved_pos;
8084            return Err(self.err("plpgsql SELECT … INTO: rebuilt stream missing SELECT".into()));
8085        }
8086        let sel = self.parse_select_stmt();
8087        self.tokens = saved_tokens;
8088        self.pos = end;
8089        let sel = sel?;
8090        let Statement::Select(body) = sel else {
8091            return Err(self.err(alloc::format!(
8092                "plpgsql SELECT … INTO: rebuilt SELECT did not produce a Select node, got {sel:?}"
8093            )));
8094        };
8095        Ok(Some((body, var)))
8096    }
8097
8098    fn parse_plpgsql_assign_target(&mut self) -> Result<AssignTarget, ParseError> {
8099        // v7.16.1 — read the head token DIRECTLY rather than
8100        // via `expect_ident_like`. The v7.14.0 schema-qualifier
8101        // strip (`public.t` → `t`) inside `expect_ident_like`
8102        // greedily consumes any `ident . ident` pair, which
8103        // silently turned every `NEW.col := …` /
8104        // `OLD.col := …` plpgsql assignment into a Local("col")
8105        // assignment — the head "new"/"old" was eaten as if it
8106        // were a schema name and the Dot was consumed too, so
8107        // this function's own `peek() == Token::Dot` check
8108        // below never fired. Every BEFORE trigger that rewrote
8109        // a NEW cell was a silent no-op for two major releases
8110        // (v7.14.0 + v7.15.0) until the e2e_trigger workspace-
8111        // gate failures were investigated as v7.16.1 backlog.
8112        let head = match self.advance() {
8113            Token::Ident(s) | Token::QuotedIdent(s) => s,
8114            other => {
8115                return Err(self.err(alloc::format!(
8116                    "expected NEW / OLD / <local_var> as plpgsql assign target, got {other:?}"
8117                )));
8118            }
8119        };
8120        if matches!(self.peek(), Token::Dot) {
8121            self.advance();
8122            let col = self.expect_ident_like()?;
8123            if head.eq_ignore_ascii_case("new") {
8124                return Ok(AssignTarget::NewColumn(col));
8125            }
8126            if head.eq_ignore_ascii_case("old") {
8127                return Ok(AssignTarget::OldColumn(col));
8128            }
8129            return Err(self.err(alloc::format!(
8130                "plpgsql assign target must be NEW.<col> / OLD.<col> / <local_var>; \
8131                 got {head:?}.<col>"
8132            )));
8133        }
8134        Ok(AssignTarget::Local(head))
8135    }
8136
8137    fn parse_plpgsql_return(&mut self) -> Result<PlPgSqlStmt, ParseError> {
8138        // RETURN NEW / OLD / NULL — bare-ident forms.
8139        match self.peek() {
8140            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("new") => {
8141                self.advance();
8142                return Ok(PlPgSqlStmt::Return(ReturnTarget::New));
8143            }
8144            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("old") => {
8145                self.advance();
8146                return Ok(PlPgSqlStmt::Return(ReturnTarget::Old));
8147            }
8148            Token::Null => {
8149                self.advance();
8150                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8151            }
8152            // Bare `RETURN;` (no value) — treated as `RETURN NULL`
8153            // per PL/pgSQL convention.
8154            Token::Semicolon => {
8155                return Ok(PlPgSqlStmt::Return(ReturnTarget::Null));
8156            }
8157            _ => {}
8158        }
8159        // v7.37.20 (20.11) — RETURN QUERY <select> / RETURN QUERY
8160        // EXECUTE <expr>. In a DO block context RETURN QUERY has no
8161        // caller-visible effect (blocks don't return sets), so we
8162        // desugar it identically to PERFORM: parse the SELECT (or
8163        // EXECUTE dynamic) as embedded SQL that runs for side
8164        // effects and discards the result. RETURN NEXT <expr>
8165        // (single-row accumulator) queues with v7.40 SETOF function
8166        // infrastructure.
8167        // v7.39 (read01 round 66) — `RETURN NEXT <expr>`: append a row to the set
8168        // and keep going.
8169        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("next"))
8170        {
8171            self.advance();
8172            let e = self.parse_expr(0)?;
8173            return Ok(PlPgSqlStmt::ReturnNext(e));
8174        }
8175        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("query"))
8176        {
8177            self.advance();
8178            // v7.39 (read01 round 68) — `RETURN QUERY EXECUTE <sql expr>`: the
8179            // rows go to the set, like the static form. It used to desugar to a
8180            // bare ExecuteDynamic, whose result was DISCARDED.
8181            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("execute"))
8182            {
8183                self.advance();
8184                let sql = self.parse_expr(0)?;
8185                return Ok(PlPgSqlStmt::ReturnQueryExecute { sql });
8186            }
8187            // Bare RETURN QUERY <select>. If the current token is
8188            // not already SELECT (e.g., the user wrote `RETURN QUERY
8189            // <projection> FROM ...` in a shorthand — rare but PG
8190            // accepts a bare projection here), splice one in. Same
8191            // trick as PERFORM.
8192            if !matches!(self.peek(), Token::Select) {
8193                self.tokens.insert(self.pos, Token::Select);
8194            }
8195            let select = self.parse_select_stmt()?;
8196            let Statement::Select(s) = select else {
8197                return Err(self.err(alloc::format!(
8198                    "expected SELECT body after RETURN QUERY, got {:?}",
8199                    self.peek()
8200                )));
8201            };
8202            // v7.39 (read01 round 66) — a REAL statement now. It used to desugar
8203            // to an embedded side-effect SELECT whose rows were DISCARDED, which
8204            // in a SETOF function is the entire answer thrown away.
8205            return Ok(PlPgSqlStmt::ReturnQuery(Box::new(s)));
8206        }
8207        // Fall through: parse a full expression.
8208        let e = self.parse_expr(0)?;
8209        Ok(PlPgSqlStmt::Return(ReturnTarget::Expr(e)))
8210    }
8211
8212    fn parse_trigger_event(&mut self) -> Result<TriggerEvent, ParseError> {
8213        // INSERT is a reserved Token; UPDATE / DELETE / TRUNCATE
8214        // are ident-shaped (the parser keys off case-insensitive
8215        // match — same shape used by the top-level Update / Delete
8216        // dispatchers at parse_one_statement).
8217        if matches!(self.peek(), Token::Insert) {
8218            self.advance();
8219            return Ok(TriggerEvent::Insert);
8220        }
8221        match self.peek() {
8222            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
8223                self.advance();
8224                Ok(TriggerEvent::Update)
8225            }
8226            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
8227                self.advance();
8228                Ok(TriggerEvent::Delete)
8229            }
8230            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("truncate") => {
8231                self.advance();
8232                Ok(TriggerEvent::Truncate)
8233            }
8234            other => Err(self.err(alloc::format!(
8235                "expected INSERT / UPDATE / DELETE / TRUNCATE in trigger event list, got {other:?}"
8236            ))),
8237        }
8238    }
8239
8240    /// v6.1.2 → v6.1.3 — `CREATE PUBLICATION <name>` body. Accepts:
8241    ///   - (no clause) → implicit `FOR ALL TABLES`
8242    ///   - `FOR ALL TABLES`
8243    ///   - `FOR ALL TABLES EXCEPT t1, t2, …` (v6.1.3)
8244    ///   - `FOR TABLE t1, t2, …` (v6.1.3) — `FOR TABLES …` also
8245    ///     accepted as an SPG lenience. PG18-measured (round 753): PG
8246    ///     REJECTS the bare plural (`invalid publication object list`,
8247    ///     TABLES only pairs with IN SCHEMA); the old note claimed an
8248    ///     unverifiable "PG 19 accepts both". Ledgered, not load-bearing.
8249    fn parse_create_publication_after_keyword(&mut self) -> Result<Statement, ParseError> {
8250        let name = self.expect_ident_or_string()?;
8251        // Bare DDL maps to FOR ALL TABLES — matches the v6.1.2
8252        // shape so existing publications keep parsing identically.
8253        let scope = if matches!(self.peek(), Token::For) {
8254            self.advance();
8255            if matches!(self.peek(), Token::All) {
8256                self.advance();
8257                if !matches!(self.peek(), Token::Tables) {
8258                    return Err(self.err(format!(
8259                        "expected TABLES after FOR ALL, got {:?}",
8260                        self.peek()
8261                    )));
8262                }
8263                self.advance();
8264                if matches!(self.peek(), Token::Except) {
8265                    self.advance();
8266                    let tables = self.parse_publication_table_list()?;
8267                    PublicationScope::AllTablesExcept(tables)
8268                } else {
8269                    PublicationScope::AllTables
8270                }
8271            } else if matches!(self.peek(), Token::Table) {
8272                self.advance();
8273                let tables = self.parse_publication_table_list()?;
8274                PublicationScope::ForTables(tables)
8275            } else if matches!(self.peek(), Token::Tables) {
8276                // v7.39 (round 754, F31-B5) — PG18-measured: the bare
8277                // plural (`FOR TABLES t`) is REJECTED (`invalid
8278                // publication object list`); TABLES only pairs with
8279                // `IN SCHEMA`. The old arm accepted it on an
8280                // unverifiable "PG 19 accepts both" claim.
8281                self.advance();
8282                if !matches!(self.peek(), Token::In) {
8283                    return Err(self.err(alloc::string::String::from(
8284                        "invalid publication object list",
8285                    )));
8286                }
8287                self.advance();
8288                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("schema")) {
8289                    return Err(self.err(format!(
8290                        "expected SCHEMA after FOR TABLES IN, got {:?}",
8291                        self.peek()
8292                    )));
8293                }
8294                self.advance();
8295                let schema = self.expect_ident_or_string()?;
8296                PublicationScope::TablesInSchema(schema)
8297            } else {
8298                return Err(self.err(format!(
8299                    "expected ALL TABLES or TABLE <list> after FOR, got {:?}",
8300                    self.peek()
8301                )));
8302            }
8303        } else {
8304            PublicationScope::AllTables
8305        };
8306        Ok(Statement::CreatePublication(CreatePublicationStatement {
8307            name,
8308            scope,
8309        }))
8310    }
8311
8312    /// v6.1.3 — Comma-separated identifier list for the publication
8313    /// FOR-clause. Requires at least one entry; empty list is a
8314    /// parse error (PG behaviour). Quoted idents are accepted; the
8315    /// names round-trip through `Display` as `quote_ident(name)`.
8316    ///
8317    /// v7.37.21 (21.2 + 21.3) — accept-and-discard the per-table
8318    /// `(col_list) WHERE (predicate)` modifiers PG 15+ emits in
8319    /// pg_dump output. SPG's publication state today is per-table
8320    /// only (matching the pre-PG-15 surface); the col list + WHERE
8321    /// are parsed so dumps load through and the table name reaches
8322    /// `PublicationScope::ForTables`, but the filter is not enforced
8323    /// at publish time. Re-open when a customer dogfood gate
8324    /// requires per-row-filter or column-subset publish semantics
8325    /// (which gates on persistent slot state landing first, 21.12).
8326    fn parse_publication_table_list(&mut self) -> Result<Vec<String>, ParseError> {
8327        let first = self.parse_publication_table_entry()?;
8328        let mut out = alloc::vec![first];
8329        while matches!(self.peek(), Token::Comma) {
8330            self.advance();
8331            out.push(self.parse_publication_table_entry()?);
8332        }
8333        Ok(out)
8334    }
8335
8336    /// One table entry inside a FOR TABLE clause:
8337    ///     tab_name [ (col, col, …) ] [ WHERE (predicate) ]
8338    /// Returns just the table name; the column list + WHERE predicate
8339    /// are consumed and discarded per the parse-accept-discard
8340    /// commitment above.
8341    fn parse_publication_table_entry(&mut self) -> Result<String, ParseError> {
8342        let name = self.expect_ident_like()?;
8343        // Optional column list — `(col, col, …)`.
8344        if matches!(self.peek(), Token::LParen) {
8345            self.advance();
8346            // Empty parens are a PG error too; require ≥ 1 column.
8347            let _ = self.expect_ident_like()?;
8348            while matches!(self.peek(), Token::Comma) {
8349                self.advance();
8350                let _ = self.expect_ident_like()?;
8351            }
8352            if !matches!(self.peek(), Token::RParen) {
8353                return Err(self.err(alloc::format!(
8354                    "expected ')' to close publication column list, got {:?}",
8355                    self.peek()
8356                )));
8357            }
8358            self.advance();
8359        }
8360        // Optional row filter — `WHERE (predicate)`.
8361        if matches!(self.peek(), Token::Where) {
8362            self.advance();
8363            if !matches!(self.peek(), Token::LParen) {
8364                return Err(self.err(alloc::format!(
8365                    "expected '(' after WHERE in publication row filter, got {:?}",
8366                    self.peek()
8367                )));
8368            }
8369            self.advance();
8370            let _ = self.parse_expr(0)?;
8371            if !matches!(self.peek(), Token::RParen) {
8372                return Err(self.err(alloc::format!(
8373                    "expected ')' to close publication WHERE filter, got {:?}",
8374                    self.peek()
8375                )));
8376            }
8377            self.advance();
8378        }
8379        Ok(name)
8380    }
8381
8382    /// v6.1.4 — `CREATE SUBSCRIPTION <name>
8383    ///                 CONNECTION '<conn>'
8384    ///                 PUBLICATION <pub> [, <pub> ...]`.
8385    ///
8386    /// The clause order is fixed (CONNECTION first, then
8387    /// PUBLICATION) to match PG. No WITH-options accepted in
8388    /// v6.1.4 — `enabled` defaults to true, no other knobs ship.
8389    fn parse_create_subscription_after_keyword(&mut self) -> Result<Statement, ParseError> {
8390        let name = self.expect_ident_or_string()?;
8391        if !matches!(self.peek(), Token::Connection) {
8392            return Err(self.err(format!(
8393                "expected CONNECTION after CREATE SUBSCRIPTION <name>, got {:?}",
8394                self.peek()
8395            )));
8396        }
8397        self.advance();
8398        let conn_str = self.expect_string_literal()?;
8399        if !matches!(self.peek(), Token::Publication) {
8400            return Err(self.err(format!(
8401                "expected PUBLICATION after CONNECTION '<conn>', got {:?}",
8402                self.peek()
8403            )));
8404        }
8405        self.advance();
8406        // Reuse the publication FOR-list parser shape: at least one
8407        // identifier, comma-separated.
8408        let first = self.expect_ident_like()?;
8409        let mut publications = alloc::vec![first];
8410        while matches!(self.peek(), Token::Comma) {
8411            self.advance();
8412            publications.push(self.expect_ident_like()?);
8413        }
8414        Ok(Statement::CreateSubscription(CreateSubscriptionStatement {
8415            name,
8416            conn_str,
8417            publications,
8418        }))
8419    }
8420
8421    /// v6.1.7 — `WAIT FOR WAL POSITION <pos> [WITH TIMEOUT <ms>]`.
8422    /// All keywords after `WAIT` are bare idents in v6.1.x; no
8423    /// lexer churn. Both `<pos>` and `<ms>` are positive integers
8424    /// that fit `u64`.
8425    /// Parameter name in `SET <name>`. A GUC name may be dotted, but the
8426    /// qualifier is a *namespace* the app owns (`app.user_id`,
8427    /// `myapp.tenant` — the request-context / RLS pattern), NOT a schema
8428    /// to discard. So parse the raw segments here instead of
8429    /// `expect_ident_like`, which strips a leading `schema.` qualifier
8430    /// and would collapse `SET app.foo` to just `foo`. Standard GUCs are
8431    /// a single segment and round-trip unchanged.
8432    fn parse_set_param_name(&mut self) -> Result<String, ParseError> {
8433        let mut parts: alloc::vec::Vec<String> = alloc::vec::Vec::new();
8434        loop {
8435            let seg = match self.advance() {
8436                Token::Ident(s) | Token::QuotedIdent(s) => s,
8437                other if unreserved_keyword_text(&other).is_some() => {
8438                    unreserved_keyword_text(&other).unwrap()
8439                }
8440                other => {
8441                    return Err(ParseError {
8442                        message: format!("expected parameter name, got {other:?}"),
8443                        token_pos: self.consumed_pos(),
8444                    });
8445                }
8446            };
8447            parts.push(seg);
8448            if matches!(self.peek(), Token::Dot) {
8449                self.advance();
8450                continue;
8451            }
8452            break;
8453        }
8454        Ok(parts.join(".").to_ascii_lowercase())
8455    }
8456
8457    fn parse_set_value(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8458        Self::parse_set_value_inner(self)
8459    }
8460
8461    fn parse_set_value_inner(&mut self) -> Result<crate::ast::SetValue, ParseError> {
8462        match self.advance() {
8463            Token::String(s) => Ok(crate::ast::SetValue::String(s)),
8464            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("default") => {
8465                Ok(crate::ast::SetValue::Default)
8466            }
8467            Token::Ident(s) | Token::QuotedIdent(s) => {
8468                let mut accum = s;
8469                while matches!(self.peek(), Token::Dot) {
8470                    self.advance();
8471                    let next = self.expect_ident_like()?;
8472                    accum.push('.');
8473                    accum.push_str(&next);
8474                }
8475                Ok(crate::ast::SetValue::Ident(accum))
8476            }
8477            Token::Integer(n) => Ok(crate::ast::SetValue::Number(n.to_string())),
8478            Token::Float(f) => Ok(crate::ast::SetValue::Number(f.to_string())),
8479            // v7.22 (mailrs round-13 gap 2) — PG boolean parameter
8480            // spellings that lex as keyword tokens, not idents:
8481            // `SET standard_conforming_strings = on` is in every
8482            // pg_dump preamble (`off` already lexes as an ident).
8483            // v7.39 (round 769, F31 tranche 5 #150) — `SET x TO DEFAULT`:
8484            // DEFAULT lexes as its keyword token, so the ident arm above
8485            // never saw it and the everyday reset form was a syntax error.
8486            Token::Default => Ok(crate::ast::SetValue::Default),
8487            // v7.40.11 — MySQL 9.7.2 accepts `NULL` here for exactly one
8488            // variable and rejects it with error 1231 for every other
8489            // (both measured); PostgreSQL 18.6 rejects the token itself
8490            // with `syntax error at or near "NULL"`. So the token is
8491            // admitted only for a MySQL session and the per-variable
8492            // decision is the executor's.
8493            Token::Null if self.mysql_dialect => Ok(crate::ast::SetValue::Null),
8494            Token::On => Ok(crate::ast::SetValue::Ident("on".to_string())),
8495            Token::True => Ok(crate::ast::SetValue::Ident("true".to_string())),
8496            Token::False => Ok(crate::ast::SetValue::Ident("false".to_string())),
8497            // v7.14.0 — MySQL session/user variable RHS
8498            // (e.g. `SET OLD_FOREIGN_KEY_CHECKS = @@FOREIGN_KEY_CHECKS`).
8499            // Wrap as Ident so the SET handler can record it; the
8500            // engine treats `@VAR` / `@@VAR` values as opaque
8501            // strings.
8502            Token::SessionVar(s) => Ok(crate::ast::SetValue::Ident(s)),
8503            // v7.14.0 — `SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO,STRICT_TRANS_TABLES'`
8504            // is the common MySQL preamble shape. Allow a `+` or
8505            // `-` prefix on negative numerics for parity with PG
8506            // (some param defaults are negative).
8507            Token::Minus => match self.advance() {
8508                Token::Integer(n) => Ok(crate::ast::SetValue::Number(alloc::format!("-{n}"))),
8509                Token::Float(f) => Ok(crate::ast::SetValue::Number(alloc::format!("-{f}"))),
8510                other => Err(self.err(format!(
8511                    "expected numeric after `-` in SET value, got {other:?}"
8512                ))),
8513            },
8514            other => Err(self.err(format!(
8515                "expected literal, identifier, or DEFAULT after `=` in SET, got {other:?}"
8516            ))),
8517        }
8518    }
8519
8520    /// v7.38 轴 4 — `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8521    /// [[NOT] DEFERRABLE]` modes after `SET TRANSACTION` or
8522    /// `START TRANSACTION` / `BEGIN`. Returns the isolation level
8523    /// (default `ReadCommitted` if no `ISOLATION LEVEL` clause was
8524    /// present). Modes are comma-separated per PG; SPG also
8525    /// accepts space-separated for tolerance. READ ONLY / WRITE
8526    /// / DEFERRABLE are parsed-and-ignored (recorded for future
8527    /// surface but not behaviorally honoured today).
8528    /// Parse the trailing `[ISOLATION LEVEL …] [READ ONLY|WRITE]
8529    /// [[NOT] DEFERRABLE]` modes of BEGIN / START TRANSACTION / SET
8530    /// TRANSACTION. Returns `Some(level)` only when an explicit `ISOLATION
8531    /// LEVEL` clause was given, so a bare `BEGIN` / `BEGIN READ ONLY` keeps the
8532    /// session default rather than forcing READ COMMITTED.
8533    fn parse_isolation_level_clauses(
8534        &mut self,
8535    ) -> Result<crate::ast::TransactionModes, ParseError> {
8536        let mut level = IsolationLevel::default();
8537        let mut have_level = false;
8538        // v7.39 — READ ONLY / READ WRITE used to be consumed and dropped,
8539        // so `BEGIN READ ONLY` opened an ordinary read-write transaction.
8540        let mut read_only: Option<bool> = None;
8541        loop {
8542            // ISOLATION LEVEL …
8543            let saw_isolation =
8544                matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("isolation"));
8545            if saw_isolation {
8546                self.advance(); // ISOLATION
8547                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("level")) {
8548                    return Err(self.err(alloc::format!(
8549                        "expected LEVEL after ISOLATION, got {:?}",
8550                        self.peek()
8551                    )));
8552                }
8553                self.advance(); // LEVEL
8554                // SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED
8555                let w1 = self
8556                    .expect_ident_like()
8557                    .map_err(|e| self.err(alloc::format!("isolation level: {e:?}")))?;
8558                let lc = w1.to_ascii_lowercase();
8559                level = match lc.as_str() {
8560                    "serializable" => IsolationLevel::Serializable,
8561                    "repeatable" => {
8562                        // Expect READ
8563                        let w2 = self
8564                            .expect_ident_like()
8565                            .map_err(|e| self.err(alloc::format!("REPEATABLE …: {e:?}")))?;
8566                        if !w2.eq_ignore_ascii_case("read") {
8567                            return Err(self.err(alloc::format!(
8568                                "expected READ after REPEATABLE, got {w2:?}"
8569                            )));
8570                        }
8571                        IsolationLevel::RepeatableRead
8572                    }
8573                    "read" => {
8574                        let w2 = self
8575                            .expect_ident_like()
8576                            .map_err(|e| self.err(alloc::format!("READ …: {e:?}")))?;
8577                        match w2.to_ascii_lowercase().as_str() {
8578                            "committed" => IsolationLevel::ReadCommitted,
8579                            "uncommitted" => IsolationLevel::ReadUncommitted,
8580                            other => {
8581                                return Err(self.err(alloc::format!(
8582                                    "expected COMMITTED or UNCOMMITTED after READ, got {other:?}"
8583                                )));
8584                            }
8585                        }
8586                    }
8587                    other => {
8588                        return Err(self.err(alloc::format!("unknown isolation level {other:?}")));
8589                    }
8590                };
8591                have_level = true;
8592            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("read")) {
8593                // v7.39 — READ ONLY | READ WRITE. The comment here used to
8594                // read "parsed, not behaviorally honoured", and it was
8595                // accurate: the clause was thrown away, so `BEGIN READ ONLY`
8596                // opened an ordinary read-write transaction and accepted
8597                // every write in it.
8598                self.advance();
8599                match self.peek().clone() {
8600                    Token::Ident(s) if s.eq_ignore_ascii_case("only") => {
8601                        self.advance();
8602                        read_only = Some(true);
8603                    }
8604                    Token::Ident(s) if s.eq_ignore_ascii_case("write") => {
8605                        self.advance();
8606                        read_only = Some(false);
8607                    }
8608                    other => {
8609                        return Err(self.err(alloc::format!(
8610                            "expected ONLY or WRITE after READ, got {other:?}"
8611                        )));
8612                    }
8613                }
8614            } else if matches!(self.peek(), Token::Not) {
8615                // NOT DEFERRABLE — `NOT` lexes as a reserved keyword.
8616                self.advance();
8617                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable")) {
8618                    return Err(self.err(alloc::format!(
8619                        "expected DEFERRABLE after NOT, got {:?}",
8620                        self.peek()
8621                    )));
8622                }
8623                self.advance();
8624            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable"))
8625            {
8626                self.advance();
8627            } else {
8628                break;
8629            }
8630            // Optional comma between modes.
8631            if matches!(self.peek(), Token::Comma) {
8632                self.advance();
8633            }
8634        }
8635        Ok(crate::ast::TransactionModes {
8636            isolation: have_level.then_some(level),
8637            read_only,
8638        })
8639    }
8640
8641    fn parse_wait_after_keyword(&mut self) -> Result<Statement, ParseError> {
8642        // FOR is a v6.1.2-reserved keyword (Token::For). The
8643        // other two are bare idents — they've never needed lexer
8644        // support and we keep it that way.
8645        if !matches!(self.peek(), Token::For) {
8646            return Err(self.err(format!("expected FOR after WAIT, got {:?}", self.peek())));
8647        }
8648        self.advance();
8649        self.expect_keyword_ident("wal")?;
8650        self.expect_keyword_ident("position")?;
8651        let pos = self.expect_u64_literal()?;
8652        let timeout_ms = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8653        {
8654            self.advance();
8655            self.expect_keyword_ident("timeout")?;
8656            Some(self.expect_u64_literal()?)
8657        } else {
8658            None
8659        };
8660        Ok(Statement::WaitForWalPosition { pos, timeout_ms })
8661    }
8662
8663    /// v6.1.7 helper — consume a `Token::Integer` and check it
8664    /// fits `u64`. WAL positions and millisecond timeouts are
8665    /// non-negative.
8666    fn expect_u64_literal(&mut self) -> Result<u64, ParseError> {
8667        match self.advance() {
8668            Token::Integer(n) if n >= 0 => Ok(n as u64),
8669            Token::Integer(n) => Err(ParseError {
8670                message: format!("expected non-negative integer, got {n}"),
8671                token_pos: self.consumed_pos(),
8672            }),
8673            other => Err(ParseError {
8674                message: format!("expected integer literal, got {other:?}"),
8675                token_pos: self.consumed_pos(),
8676            }),
8677        }
8678    }
8679
8680    /// `CREATE USER` body — name + WITH PASSWORD '<pw>' + optional
8681    /// ROLE '<role>' (defaults to readonly). All string slots accept
8682    /// either a quoted ident or a quoted string literal.
8683    /// `CREATE {USER|ROLE} name [WITH] [PASSWORD 'x'] [LOGIN|NOLOGIN]
8684    /// [INHERIT|NOINHERIT] [SUPERUSER|NOSUPERUSER] [ROLE 'admin']`.
8685    ///
8686    /// `is_user` = the statement said USER, which in PG means LOGIN by default.
8687    /// The legacy SPG `ROLE 'readwrite'` clause (the coarse read/write/admin
8688    /// wire role) still parses — it is a different axis from the PG attributes.
8689    /// v7.39 (round 547) — is this ALTER ROLE / DATABASE one of the
8690    /// SET forms? Peeks past the name (and an `IN DATABASE d`) for SET
8691    /// or RESET, so the plain attribute forms keep their old path.
8692    fn peeks_db_role_setting(&self) -> bool {
8693        let mut i = self.pos + 1; // past the object's name
8694        let word = |p: usize| -> Option<String> {
8695            match self.tokens.get(p) {
8696                Some(Token::Ident(s) | Token::QuotedIdent(s)) => Some(s.to_ascii_lowercase()),
8697                Some(Token::In) => Some(String::from("in")),
8698                _ => None,
8699            }
8700        };
8701        if word(i).as_deref() == Some("in") && word(i + 1).as_deref() == Some("database") {
8702            i += 3; // IN DATABASE <name>
8703        }
8704        matches!(word(i).as_deref(), Some("set" | "reset"))
8705    }
8706
8707    fn parse_db_role_setting(&mut self, is_database: bool) -> Result<Statement, ParseError> {
8708        use crate::ast::SetDbRoleSettingStatement;
8709        // `ALTER ROLE ALL SET …` — ALL lexes as a KEYWORD, not an
8710        // identifier, so the ordinary name reader refuses it. Same trap
8711        // as TABLE / INDEX / FULL / DEFAULT before it.
8712        let name = if matches!(self.peek(), Token::All) {
8713            self.advance();
8714            String::from("all")
8715        } else {
8716            self.expect_ident_or_string()?
8717        };
8718        // `ALTER ROLE ALL SET …` is PG's every-role scope (oid 0).
8719        let all = name.eq_ignore_ascii_case("all");
8720        let (mut database, mut role) = if is_database {
8721            (Some(name), None)
8722        } else if all {
8723            (None, None)
8724        } else {
8725            (None, Some(name))
8726        };
8727        if matches!(self.peek(), Token::In) {
8728            self.advance();
8729            self.advance(); // DATABASE
8730            database = Some(self.expect_ident_or_string()?);
8731        }
8732        let resetting = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("reset"));
8733        self.advance(); // SET | RESET
8734        if resetting && matches!(self.peek(), Token::All) {
8735            self.advance();
8736            self.consume_until_statement_boundary();
8737            return Ok(Statement::SetDbRoleSetting(Box::new(
8738                SetDbRoleSettingStatement {
8739                    database,
8740                    role,
8741                    param: None,
8742                    value: None,
8743                },
8744            )));
8745        }
8746        let param = self.expect_ident_like()?;
8747        let value = if resetting {
8748            None
8749        } else {
8750            // `SET p = v` and PG's `SET p TO v` both. TO lexes as a
8751            // KEYWORD, so the ident-only check missed it and consumed
8752            // the word itself as the value — the same trap as ALL, one
8753            // clause over.
8754            if matches!(self.peek(), Token::Eq | Token::To) || self.peek_keyword_ident("to") {
8755                self.advance();
8756            }
8757            Some(self.take_guc_value())
8758        };
8759        self.consume_until_statement_boundary();
8760        Ok(Statement::SetDbRoleSetting(Box::new(
8761            SetDbRoleSettingStatement {
8762                database,
8763                role,
8764                param: Some(param),
8765                value,
8766            },
8767        )))
8768    }
8769
8770    /// The remainder of a `SET <p> = …` clause as PG renders it back:
8771    /// a quoted literal loses its quotes, a bare word or number does not.
8772    fn take_guc_value(&mut self) -> String {
8773        match self.advance() {
8774            Token::String(s) => s,
8775            Token::Integer(n) => format!("{n}"),
8776            Token::Float(f) => format!("{f}"),
8777            Token::Ident(s) | Token::QuotedIdent(s) => s,
8778            other => format!("{other:?}"),
8779        }
8780    }
8781
8782    fn parse_create_user_after_keyword(&mut self, is_user: bool) -> Result<Statement, ParseError> {
8783        let name = self.expect_ident_or_string()?;
8784        if self.peek_keyword_ident("with") {
8785            self.advance();
8786        }
8787        let mut password = String::new();
8788        let mut role = String::new();
8789        let mut login: Option<bool> = None;
8790        let mut inherit: Option<bool> = None;
8791        let mut superuser: Option<bool> = None;
8792        // Not a `while let`: the pattern would borrow `self` across the
8793        // body, which calls `self.advance()` / `self.expect_*` (&mut).
8794        #[allow(clippy::while_let_loop)]
8795        loop {
8796            let (Token::Ident(w) | Token::QuotedIdent(w)) = self.peek() else {
8797                break;
8798            };
8799            match w.to_ascii_lowercase().as_str() {
8800                "password" => {
8801                    self.advance();
8802                    password = self.expect_string_literal()?;
8803                }
8804                // PG accepts (and pg_dump emits) ENCRYPTED PASSWORD; the value
8805                // is the same slot.
8806                "encrypted" => {
8807                    self.advance();
8808                    self.expect_keyword_ident("password")?;
8809                    password = self.expect_string_literal()?;
8810                }
8811                "login" => {
8812                    self.advance();
8813                    login = Some(true);
8814                }
8815                "nologin" => {
8816                    self.advance();
8817                    login = Some(false);
8818                }
8819                "inherit" => {
8820                    self.advance();
8821                    inherit = Some(true);
8822                }
8823                "noinherit" => {
8824                    self.advance();
8825                    inherit = Some(false);
8826                }
8827                "superuser" => {
8828                    self.advance();
8829                    superuser = Some(true);
8830                }
8831                "nosuperuser" => {
8832                    self.advance();
8833                    superuser = Some(false);
8834                }
8835                // SPG's own coarse wire role: `ROLE 'readwrite'`.
8836                "role" => {
8837                    self.advance();
8838                    role = self.expect_string_literal()?;
8839                }
8840                // Every other PG role option (CREATEDB, CONNECTION LIMIT n,
8841                // VALID UNTIL '…', CREATEROLE, REPLICATION, BYPASSRLS …) is
8842                // accepted and ignored so a pg_dump role block restores. They
8843                // gate capabilities SPG does not have.
8844                "createdb" | "nocreatedb" | "createrole" | "nocreaterole" | "replication"
8845                | "noreplication" | "bypassrls" | "nobypassrls" => {
8846                    self.advance();
8847                }
8848                "connection" => {
8849                    self.advance();
8850                    self.expect_keyword_ident("limit")?;
8851                    self.advance(); // the number
8852                }
8853                "valid" => {
8854                    self.advance();
8855                    self.expect_keyword_ident("until")?;
8856                    self.expect_string_literal()?;
8857                }
8858                _ => break,
8859            }
8860        }
8861        if role.is_empty() {
8862            role = "readonly".to_string();
8863        }
8864        Ok(Statement::CreateUser(crate::ast::CreateUserStatement {
8865            name,
8866            password,
8867            role,
8868            login,
8869            inherit,
8870            superuser,
8871            is_user,
8872        }))
8873    }
8874
8875    /// v7.39 (RLS) — parenthesised policy qualifier `( <expr> )`; caller has
8876    /// consumed the USING / WITH CHECK keyword.
8877    fn parse_paren_expr(&mut self, clause: &str) -> Result<Expr, ParseError> {
8878        if !matches!(self.peek(), Token::LParen) {
8879            return Err(self.err(alloc::format!(
8880                "expected '(' after {clause}, got {:?}",
8881                self.peek()
8882            )));
8883        }
8884        self.advance();
8885        let e = self.parse_expr(0)?;
8886        if !matches!(self.peek(), Token::RParen) {
8887            return Err(self.err(alloc::format!(
8888                "expected ')' to close {clause}, got {:?}",
8889                self.peek()
8890            )));
8891        }
8892        self.advance();
8893        Ok(e)
8894    }
8895
8896    /// v7.39 (RLS) — `TO role [, role]*`; caller has consumed `TO`.
8897    fn parse_policy_roles(&mut self) -> Result<Vec<String>, ParseError> {
8898        let mut roles = Vec::new();
8899        loop {
8900            roles.push(self.expect_ident_like()?);
8901            if matches!(self.peek(), Token::Comma) {
8902                self.advance();
8903            } else {
8904                break;
8905            }
8906        }
8907        Ok(roles)
8908    }
8909
8910    /// v7.39 (RLS) — `CREATE POLICY name ON table [AS {PERMISSIVE|RESTRICTIVE}]
8911    /// [FOR cmd] [TO roles] [USING (expr)] [WITH CHECK (expr)]`. Caller consumed
8912    /// `CREATE POLICY`.
8913    fn parse_create_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
8914        use crate::ast::PolicyCmd;
8915        let name = self.expect_ident_like()?;
8916        if !matches!(self.peek(), Token::On) {
8917            return Err(self.err(alloc::format!(
8918                "expected ON after CREATE POLICY name, got {:?}",
8919                self.peek()
8920            )));
8921        }
8922        self.advance();
8923        let table = self.expect_ident_like()?;
8924
8925        let mut permissive = true;
8926        if matches!(self.peek(), Token::As) {
8927            self.advance();
8928            let w = self.expect_ident_like()?;
8929            permissive = if w.eq_ignore_ascii_case("permissive") {
8930                true
8931            } else if w.eq_ignore_ascii_case("restrictive") {
8932                false
8933            } else {
8934                return Err(self.err(alloc::format!(
8935                    "expected PERMISSIVE or RESTRICTIVE after AS, got {w:?}"
8936                )));
8937            };
8938        }
8939
8940        let mut cmd = PolicyCmd::All;
8941        if matches!(self.peek(), Token::For) {
8942            self.advance();
8943            cmd = self.parse_policy_cmd()?;
8944        }
8945
8946        let mut roles = Vec::new();
8947        if matches!(self.peek(), Token::To) {
8948            self.advance();
8949            roles = self.parse_policy_roles()?;
8950        }
8951
8952        let mut using = None;
8953        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
8954        {
8955            self.advance();
8956            using = Some(self.parse_paren_expr("USING")?);
8957        }
8958
8959        let mut with_check = None;
8960        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
8961        {
8962            self.advance();
8963            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
8964            {
8965                return Err(self.err(alloc::format!(
8966                    "expected CHECK after WITH, got {:?}",
8967                    self.peek()
8968                )));
8969            }
8970            self.advance();
8971            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
8972        }
8973
8974        // Clause-per-command matrix (PG wording).
8975        match cmd {
8976            PolicyCmd::Insert => {
8977                if using.is_some() {
8978                    return Err(self.err("only WITH CHECK expression allowed for INSERT".into()));
8979                }
8980            }
8981            PolicyCmd::Select | PolicyCmd::Delete => {
8982                if with_check.is_some() {
8983                    return Err(self.err("WITH CHECK cannot be applied to SELECT or DELETE".into()));
8984                }
8985            }
8986            PolicyCmd::Update | PolicyCmd::All => {}
8987        }
8988
8989        Ok(Statement::CreatePolicy(crate::ast::CreatePolicyStatement {
8990            name,
8991            table,
8992            permissive,
8993            cmd,
8994            roles,
8995            using,
8996            with_check,
8997        }))
8998    }
8999
9000    /// v7.39 (RLS) — the command word after `FOR`.
9001    fn parse_policy_cmd(&mut self) -> Result<crate::ast::PolicyCmd, ParseError> {
9002        use crate::ast::PolicyCmd;
9003        match self.peek().clone() {
9004            Token::All => {
9005                self.advance();
9006                Ok(PolicyCmd::All)
9007            }
9008            Token::Select => {
9009                self.advance();
9010                Ok(PolicyCmd::Select)
9011            }
9012            Token::Insert => {
9013                self.advance();
9014                Ok(PolicyCmd::Insert)
9015            }
9016            Token::Ident(s) if s.eq_ignore_ascii_case("update") => {
9017                self.advance();
9018                Ok(PolicyCmd::Update)
9019            }
9020            Token::Ident(s) if s.eq_ignore_ascii_case("delete") => {
9021                self.advance();
9022                Ok(PolicyCmd::Delete)
9023            }
9024            other => Err(self.err(alloc::format!(
9025                "expected ALL/SELECT/INSERT/UPDATE/DELETE after FOR, got {other:?}"
9026            ))),
9027        }
9028    }
9029
9030    /// v7.39 (RLS) — `ALTER POLICY name ON table { RENAME TO new | [TO roles]
9031    /// [USING (expr)] [WITH CHECK (expr)] }`. Caller consumed `ALTER POLICY`.
9032    fn parse_alter_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9033        let name = self.expect_ident_like()?;
9034        if !matches!(self.peek(), Token::On) {
9035            return Err(self.err(alloc::format!(
9036                "expected ON after ALTER POLICY name, got {:?}",
9037                self.peek()
9038            )));
9039        }
9040        self.advance();
9041        let table = self.expect_ident_like()?;
9042
9043        // RENAME TO new
9044        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("rename"))
9045        {
9046            self.advance();
9047            if !matches!(self.peek(), Token::To) {
9048                return Err(self.err(alloc::format!(
9049                    "expected TO after RENAME, got {:?}",
9050                    self.peek()
9051                )));
9052            }
9053            self.advance();
9054            let new = self.expect_ident_like()?;
9055            return Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9056                name,
9057                table,
9058                rename_to: Some(new),
9059                roles: None,
9060                using: None,
9061                with_check: None,
9062            }));
9063        }
9064
9065        let mut roles = None;
9066        if matches!(self.peek(), Token::To) {
9067            self.advance();
9068            roles = Some(self.parse_policy_roles()?);
9069        }
9070        let mut using = None;
9071        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"))
9072        {
9073            self.advance();
9074            using = Some(self.parse_paren_expr("USING")?);
9075        }
9076        let mut with_check = None;
9077        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with"))
9078        {
9079            self.advance();
9080            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("check"))
9081            {
9082                return Err(self.err(alloc::format!(
9083                    "expected CHECK after WITH, got {:?}",
9084                    self.peek()
9085                )));
9086            }
9087            self.advance();
9088            with_check = Some(self.parse_paren_expr("WITH CHECK")?);
9089        }
9090        Ok(Statement::AlterPolicy(crate::ast::AlterPolicyStatement {
9091            name,
9092            table,
9093            rename_to: None,
9094            roles,
9095            using,
9096            with_check,
9097        }))
9098    }
9099
9100    /// v7.39 (RLS) — `DROP POLICY [IF EXISTS] name ON table`. Caller consumed
9101    /// `DROP POLICY`.
9102    fn parse_drop_policy_after_keyword(&mut self) -> Result<Statement, ParseError> {
9103        let if_exists = self.consume_if_exists();
9104        let name = self.expect_ident_like()?;
9105        if !matches!(self.peek(), Token::On) {
9106            return Err(self.err(alloc::format!(
9107                "expected ON after DROP POLICY name, got {:?}",
9108                self.peek()
9109            )));
9110        }
9111        self.advance();
9112        let table = self.expect_ident_like()?;
9113        Ok(Statement::DropPolicy(crate::ast::DropPolicyStatement {
9114            name,
9115            table,
9116            if_exists,
9117        }))
9118    }
9119}
9120fn wrap_from_leaves(
9121    e: &mut Expr,
9122    names: &[String],
9123    make: &dyn Fn(Expr) -> Expr,
9124    refs: &dyn Fn(&Expr) -> bool,
9125) {
9126    if let Expr::Column(c) = e {
9127        if c.qualifier
9128            .as_deref()
9129            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q)))
9130        {
9131            let taken = core::mem::replace(e, Expr::Literal(Literal::Null));
9132            *e = make(taken);
9133        }
9134        return;
9135    }
9136    match e {
9137        Expr::Binary { lhs, rhs, .. } => {
9138            wrap_from_leaves(lhs, names, make, refs);
9139            wrap_from_leaves(rhs, names, make, refs);
9140        }
9141        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
9142            wrap_from_leaves(expr, names, make, refs)
9143        }
9144        Expr::FunctionCall { args, .. } => {
9145            for a in args.iter_mut() {
9146                wrap_from_leaves(a, names, make, refs);
9147            }
9148        }
9149        Expr::Case {
9150            operand,
9151            branches,
9152            else_branch,
9153        } => {
9154            if let Some(o) = operand.as_deref_mut() {
9155                wrap_from_leaves(o, names, make, refs);
9156            }
9157            for (w, t) in branches.iter_mut() {
9158                wrap_from_leaves(w, names, make, refs);
9159                wrap_from_leaves(t, names, make, refs);
9160            }
9161            if let Some(el) = else_branch.as_deref_mut() {
9162                wrap_from_leaves(el, names, make, refs);
9163            }
9164        }
9165        // Compound variants the walk doesn't decompose: keep the
9166        // pre-D.30 behavior — wrap the whole sub-expr if it touches
9167        // a source table, so nothing regresses.
9168        other => {
9169            if refs(other) {
9170                let taken = core::mem::replace(other, Expr::Literal(Literal::Null));
9171                *other = make(taken);
9172            }
9173        }
9174    }
9175}
9176
9177/// v7.39 (round 241) — does this expression reference any of the FROM /
9178/// USING table names (shared by the UPDATE…FROM and DELETE…USING
9179/// lowerings)?
9180fn expr_refs_tables(e: &Expr, names: &[String]) -> bool {
9181    match e {
9182        Expr::Column(c) => c
9183            .qualifier
9184            .as_deref()
9185            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9186        Expr::Binary { lhs, rhs, .. } => {
9187            expr_refs_tables(lhs, names) || expr_refs_tables(rhs, names)
9188        }
9189        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => expr_refs_tables(expr, names),
9190        Expr::FunctionCall { args, .. } => args.iter().any(|a| expr_refs_tables(a, names)),
9191        Expr::Case {
9192            operand,
9193            branches,
9194            else_branch,
9195        } => {
9196            operand
9197                .as_deref()
9198                .is_some_and(|o| expr_refs_tables(o, names))
9199                || branches
9200                    .iter()
9201                    .any(|(w, t)| expr_refs_tables(w, names) || expr_refs_tables(t, names))
9202                || else_branch
9203                    .as_deref()
9204                    .is_some_and(|el| expr_refs_tables(el, names))
9205        }
9206        _ => false,
9207    }
9208}
9209
9210impl Parser {
9211    /// v4.4 `UPDATE <table> SET col = expr [, col = expr]* [WHERE cond]`.
9212    /// Caller already consumed the leading `UPDATE` ident.
9213    /// v7.39 (round 420) — does a JOIN clause start here? Used to spot
9214    /// MySQL's multi-table `UPDATE a JOIN b ON …` / `UPDATE a LEFT JOIN b …`
9215    /// after the target name has been read. `JOIN` is a reserved token;
9216    /// the qualifiers are bare idents.
9217    fn peek_is_update_join_start(&self) -> bool {
9218        match self.peek() {
9219            // JOIN and its qualifiers are reserved lexer tokens (the grammar
9220            // dedicates arms to `LEFT [OUTER] JOIN` and friends).
9221            Token::Join
9222            | Token::Inner
9223            | Token::Left
9224            | Token::Right
9225            | Token::Cross
9226            | Token::Full => true,
9227            // NATURAL / STRAIGHT_JOIN arrive as bare idents.
9228            Token::Ident(s) | Token::QuotedIdent(s) => {
9229                matches!(s.to_ascii_lowercase().as_str(), "natural" | "straight_join")
9230            }
9231            _ => false,
9232        }
9233    }
9234
9235    /// v7.39 (round 430) — `SET @x = <expr> [, @y := <expr>]`, MySQL's
9236    /// USER-variable assignment. Its own per-session namespace, an arbitrary
9237    /// expression on the right, and `:=` as a second spelling of `=`.
9238    ///
9239    /// Out-of-line (`inline(never)`): the statement-parse frame it is called
9240    /// from sits on the nesting recursion chain (a CTE body, a subquery),
9241    /// and holding this loop's `Vec` + `String` locals there overflowed the
9242    /// 512 KiB guard (`e2e_in_list_depth::round25_union_cte_search_shape`).
9243    #[inline(never)]
9244    fn parse_set_user_vars(&mut self) -> Result<Statement, ParseError> {
9245        let mut assigns: Vec<(String, Expr)> = Vec::new();
9246        let mut settings: Vec<(String, Expr)> = Vec::new();
9247        loop {
9248            // v7.39 (round 554) — a plain NAME here is a session
9249            // setting, not a user variable. mysqldump writes the two in
9250            // one statement — `SET @OLD_SQL_MODE=@@SQL_MODE,
9251            // SQL_MODE='NO_AUTO_VALUE_ON_ZERO'` saves a value and
9252            // changes it — and this refused the mixture outright, so no
9253            // dump could be restored past its preamble.
9254            if let Token::Ident(name) | Token::QuotedIdent(name) = self.peek().clone() {
9255                self.advance();
9256                if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9257                    return Err(self.err(alloc::format!(
9258                        "expected `=` after {name}, got {:?}",
9259                        self.peek()
9260                    )));
9261                }
9262                self.advance();
9263                let value = self.parse_expr(0)?;
9264                settings.push((name.to_ascii_lowercase(), value));
9265                if matches!(self.peek(), Token::Comma) {
9266                    self.advance();
9267                    continue;
9268                }
9269                break;
9270            }
9271            let Token::SessionVar(raw) = self.peek().clone() else {
9272                return Err(self.err(alloc::format!(
9273                    "expected a user variable after SET, got {:?}",
9274                    self.peek()
9275                )));
9276            };
9277            if raw.starts_with("@@") {
9278                return Err(self.err(alloc::string::String::from(
9279                    "cannot mix `@@` settings with `@` user variables in one SET",
9280                )));
9281            }
9282            self.advance();
9283            if !matches!(self.peek(), Token::Eq | Token::ColonEq) {
9284                return Err(self.err(alloc::format!(
9285                    "expected `=` or `:=` after {raw}, got {:?}",
9286                    self.peek()
9287                )));
9288            }
9289            self.advance();
9290            let value = self.parse_expr(0)?;
9291            assigns.push((raw.trim_start_matches('@').to_ascii_lowercase(), value));
9292            if matches!(self.peek(), Token::Comma) {
9293                self.advance();
9294                continue;
9295            }
9296            break;
9297        }
9298        Ok(Statement::SetUserVars(assigns, settings))
9299    }
9300
9301    fn parse_update_after_keyword(&mut self) -> Result<Statement, ParseError> {
9302        // v7.39 (round 646) — `UPDATE ONLY t SET …`. Read as a table
9303        // NAMED `only` until now, which failed on `relation "only" does
9304        // not exist`. The lookahead is what keeps a table actually
9305        // called `only` working: the keyword is only a keyword when a
9306        // TABLE NAME follows it — and `SET` arrives as an identifier
9307        // here, so `UPDATE only SET a = 2` would otherwise take `SET`
9308        // for the table and die on the `=`. Measured by the pin.
9309        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9310            if s.eq_ignore_ascii_case("only"))
9311            && matches!(
9312                self.tokens.get(self.pos + 1),
9313                Some(Token::Ident(n) | Token::QuotedIdent(n)) if !n.eq_ignore_ascii_case("set")
9314            );
9315        if only {
9316            self.advance();
9317        }
9318        let table = self.expect_ident_like()?;
9319        // v7.39 (round 241) — `UPDATE t [AS] alias SET …`. PG allows the
9320        // bare spelling; a bare identifier that is the SET keyword itself
9321        // is the clause, not an alias.
9322        // v7.39 (round 420) — nor is a bare join qualifier (`LEFT` / `INNER`
9323        // / …) an alias: `UPDATE a LEFT JOIN b …` starts the MySQL
9324        // multi-table form, and swallowing `LEFT` as `a`'s alias made the
9325        // following JOIN a syntax error.
9326        let starts_join = self.mysql_dialect && self.peek_is_update_join_start();
9327        let alias = if matches!(self.peek(), Token::As) {
9328            self.advance();
9329            Some(self.expect_ident_like()?)
9330        } else {
9331            match self.peek() {
9332                Token::Ident(s) | Token::QuotedIdent(s)
9333                    if !s.eq_ignore_ascii_case("set") && !starts_join =>
9334                {
9335                    let a = s.clone();
9336                    self.advance();
9337                    Some(a)
9338                }
9339                _ => None,
9340            }
9341        };
9342        // v7.39 (round 420) — MySQL's multi-table UPDATE:
9343        //     UPDATE a, b        SET a.v = b.v WHERE a.id = b.id
9344        //     UPDATE a JOIN b ON a.id = b.id      SET a.v = b.v + 1
9345        //     UPDATE a LEFT JOIN b ON a.id = b.id SET a.v = COALESCE(b.v, -1)
9346        // The FIRST table is the mutation target and the rest are sources —
9347        // exactly the shape PG spells `UPDATE a SET … FROM b WHERE …`, which
9348        // SPG already lowers onto correlated subqueries. So rewind, let
9349        // `parse_from_clause` read the whole list (it handles aliases, comma
9350        // lists, and every JOIN form), then peel the target off the front.
9351        let (mysql_from, mysql_on, mysql_outer) = if self.mysql_dialect
9352            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9353        {
9354            // NOTE: `advance()` destroys the tokens it returns
9355            // (`mem::replace(.., Eof)`), so re-parsing by rewinding `self.pos`
9356            // is NOT possible — the tail is read forward, once, through the
9357            // same grammar `parse_from_clause` uses after its primary.
9358            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9359            let mut joins = self.parse_from_joins(&target_qual)?;
9360            if joins.is_empty() {
9361                return Err(self.err(alloc::string::String::from(
9362                    "multi-table UPDATE needs at least one source table",
9363                )));
9364            }
9365            let head = joins.remove(0);
9366            // A LEFT join keeps every target row (the unmatched ones see NULL
9367            // on the source side), so it must NOT get the EXISTS row filter
9368            // the inner / comma forms use.
9369            let outer = matches!(head.kind, crate::ast::JoinKind::Left);
9370            let src = FromClause {
9371                primary: head.table,
9372                joins,
9373            };
9374            (Some(src), head.on, outer)
9375        } else {
9376            (None, None, false)
9377        };
9378        self.expect_keyword_ident("set")?;
9379        let mut assignments = Vec::new();
9380        loop {
9381            // `SET (a, b) = (e1, e2)` / `SET (a, b) = (SELECT x, y
9382            // …)` — the parenthesized multi-assignment. Expressions
9383            // assign positionally; a subquery RHS clones per column
9384            // keeping only the Nth projection item.
9385            if matches!(self.peek(), Token::LParen) {
9386                self.advance();
9387                let mut cols = alloc::vec![self.expect_ident_like()?];
9388                while matches!(self.peek(), Token::Comma) {
9389                    self.advance();
9390                    cols.push(self.expect_ident_like()?);
9391                }
9392                if !matches!(self.peek(), Token::RParen) {
9393                    return Err(self.err(format!(
9394                        "expected ')' after SET column list, got {:?}",
9395                        self.peek()
9396                    )));
9397                }
9398                self.advance();
9399                if !matches!(self.peek(), Token::Eq) {
9400                    return Err(self.err(format!(
9401                        "expected `=` after SET column list, got {:?}",
9402                        self.peek()
9403                    )));
9404                }
9405                self.advance();
9406                if !matches!(self.peek(), Token::LParen) {
9407                    return Err(self.err(format!(
9408                        "expected '(' after SET (…) =, got {:?}",
9409                        self.peek()
9410                    )));
9411                }
9412                self.advance();
9413                if matches!(self.peek(), Token::Select) {
9414                    let inner = match self.parse_select_stmt()? {
9415                        Statement::Select(s) => s,
9416                        other => {
9417                            return Err(self.err(alloc::format!(
9418                                "expected SELECT in SET (…) = (SELECT …), got {other:?}"
9419                            )));
9420                        }
9421                    };
9422                    if !matches!(self.peek(), Token::RParen) {
9423                        return Err(self.err(format!(
9424                            "expected ')' after SET subquery, got {:?}",
9425                            self.peek()
9426                        )));
9427                    }
9428                    self.advance();
9429                    if inner.items.len() != cols.len() {
9430                        return Err(self.err(alloc::format!(
9431                            "SET (…) = (SELECT …) arity mismatch: {} columns, {} items",
9432                            cols.len(),
9433                            inner.items.len()
9434                        )));
9435                    }
9436                    for (i, col) in cols.into_iter().enumerate() {
9437                        let mut sub = inner.clone();
9438                        sub.items = alloc::vec![sub.items[i].clone()];
9439                        assignments.push((col, Expr::ScalarSubquery(Box::new(sub))));
9440                    }
9441                } else {
9442                    let mut exprs = alloc::vec![self.parse_expr(0)?];
9443                    while matches!(self.peek(), Token::Comma) {
9444                        self.advance();
9445                        exprs.push(self.parse_expr(0)?);
9446                    }
9447                    if !matches!(self.peek(), Token::RParen) {
9448                        return Err(self.err(format!(
9449                            "expected ')' after SET row values, got {:?}",
9450                            self.peek()
9451                        )));
9452                    }
9453                    self.advance();
9454                    if exprs.len() != cols.len() {
9455                        return Err(self.err(alloc::format!(
9456                            "SET (…) = (…) arity mismatch: {} columns, {} values",
9457                            cols.len(),
9458                            exprs.len()
9459                        )));
9460                    }
9461                    for (col, e) in cols.into_iter().zip(exprs) {
9462                        assignments.push((col, e));
9463                    }
9464                }
9465                if matches!(self.peek(), Token::Comma) {
9466                    self.advance();
9467                    continue;
9468                }
9469                break;
9470            }
9471            // v7.39 (round 420) — MySQL's multi-table UPDATE qualifies its
9472            // assignment targets (`SET a.v = b.v`). `expect_ident_like`
9473            // SILENTLY strips a `<qual>.` prefix (it exists for PG's
9474            // `public.` dump qualifiers), so the qualifier has to be read off
9475            // the token stream first — otherwise `SET b.v = 888` would write
9476            // to the TARGET table's `v` while naming a source table, a
9477            // silent-wrong. A qualifier naming a SOURCE table means a
9478            // multi-TARGET update — mutating two tables in one statement —
9479            // which SPG does not model, so it is refused loudly.
9480            let set_qual: Option<String> = if mysql_from.is_some()
9481                && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
9482            {
9483                match self.peek() {
9484                    Token::Ident(s) | Token::QuotedIdent(s) => Some(s.clone()),
9485                    _ => None,
9486                }
9487            } else {
9488                None
9489            };
9490            let col = self.expect_ident_like()?;
9491            if let Some(q) = set_qual {
9492                let names_target = q.eq_ignore_ascii_case(&table)
9493                    || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(&q));
9494                if !names_target {
9495                    return Err(self.err(alloc::format!(
9496                        "multi-table UPDATE can only assign to its first table \
9497                         ({table}); `{q}.{col}` targets another table"
9498                    )));
9499                }
9500            }
9501            // v7.37 D.53 — array element assignment target `SET arr[i] = v`,
9502            // desugared to `arr = __array_assign(arr, i, v)` (mirrors the
9503            // `__column_default` marker lowering just below). PG assigns to the
9504            // i-th (1-based) element, NULL-padding when i exceeds the length.
9505            if matches!(self.peek(), Token::LBracket) {
9506                self.advance();
9507                let index = self.parse_expr(0)?;
9508                // v7.39 (round 257) — the SLICE target `SET arr[lo:hi] = src`
9509                // (and the open `arr[lo:]`), lowered to
9510                // `__array_assign_slice`. Only the single-subscript form
9511                // parsed before, so a slice assignment was a syntax error.
9512                let mut slice_hi: Option<Option<Expr>> = None;
9513                if matches!(self.peek(), Token::Colon) {
9514                    self.advance();
9515                    slice_hi = Some(if matches!(self.peek(), Token::RBracket) {
9516                        None
9517                    } else {
9518                        Some(self.parse_expr(0)?)
9519                    });
9520                }
9521                if !matches!(self.peek(), Token::RBracket) {
9522                    return Err(self.err(format!(
9523                        "expected `]` after array subscript in UPDATE SET, got {:?}",
9524                        self.peek()
9525                    )));
9526                }
9527                self.advance();
9528                if !matches!(self.peek(), Token::Eq) {
9529                    return Err(self.err(format!(
9530                        "expected `=` after array subscript in UPDATE SET, got {:?}",
9531                        self.peek()
9532                    )));
9533                }
9534                self.advance();
9535                let value = self.parse_expr(0)?;
9536                // PG merges several subscript writes to the same column into one
9537                // array (`SET arr[1]=x, arr[3]=y`), so chain onto any prior
9538                // assignment to `col` rather than each overwriting the original.
9539                let existing = assignments.iter().position(|(c, _)| c == &col);
9540                let base = match existing {
9541                    Some(i) => assignments[i].1.clone(),
9542                    None => Expr::Column(ColumnName {
9543                        qualifier: None,
9544                        name: col.clone(),
9545                    }),
9546                };
9547                let assigned = match slice_hi {
9548                    None => Expr::FunctionCall {
9549                        name: "__array_assign".to_string(),
9550                        args: alloc::vec![base, index, value],
9551                    },
9552                    Some(hi) => Expr::FunctionCall {
9553                        name: "__array_assign_slice".to_string(),
9554                        args: alloc::vec![
9555                            base,
9556                            index,
9557                            hi.unwrap_or(Expr::Literal(crate::ast::Literal::Null)),
9558                            value,
9559                        ],
9560                    },
9561                };
9562                match existing {
9563                    Some(i) => assignments[i].1 = assigned,
9564                    None => assignments.push((col, assigned)),
9565                }
9566                if matches!(self.peek(), Token::Comma) {
9567                    self.advance();
9568                    continue;
9569                }
9570                break;
9571            }
9572            if !matches!(self.peek(), Token::Eq) {
9573                return Err(self.err(format!(
9574                    "expected `=` after column name in UPDATE SET, got {:?}",
9575                    self.peek()
9576                )));
9577            }
9578            self.advance();
9579            // `SET col = DEFAULT` — the column's declared default;
9580            // rides out as a marker call the update executor
9581            // resolves against the schema.
9582            let value = if matches!(self.peek(), Token::Default) {
9583                self.advance();
9584                Expr::FunctionCall {
9585                    name: "__column_default".to_string(),
9586                    args: Vec::new(),
9587                }
9588            } else {
9589                self.parse_expr(0)?
9590            };
9591            assignments.push((col, value));
9592            if matches!(self.peek(), Token::Comma) {
9593                self.advance();
9594                continue;
9595            }
9596            break;
9597        }
9598        // `UPDATE t SET … FROM src [, …] WHERE cond` — PG's joined
9599        // update. Lowers onto the correlated-subquery machinery:
9600        // the WHERE becomes EXISTS(SELECT 1 FROM src WHERE cond)
9601        // and each assignment that references a FROM-list table
9602        // wraps into a correlated scalar subquery
9603        // (SELECT expr FROM src WHERE cond). Equivalent for the
9604        // unique-join shape (the overwhelmingly common one); a
9605        // multi-match, which PG resolves by arbitrary pick,
9606        // surfaces as a scalar-subquery cardinality error instead
9607        // of a silent arbitrary result.
9608        // v7.39 (round 420) — the MySQL multi-table form supplies the source
9609        // list up front (`UPDATE a, b SET …`) instead of via FROM, so it feeds
9610        // the SAME lowering below. Both spellings together is not legal in
9611        // either dialect.
9612        let from_clause = if let Some(fc) = mysql_from {
9613            if matches!(self.peek(), Token::From) {
9614                return Err(self.err(alloc::string::String::from(
9615                    "multi-table UPDATE already names its sources; drop the FROM clause",
9616                )));
9617            }
9618            Some(fc)
9619        } else if matches!(self.peek(), Token::From) {
9620            self.advance();
9621            Some(self.parse_from_clause()?)
9622        } else {
9623            None
9624        };
9625        let where_ = if matches!(self.peek(), Token::Where) {
9626            self.advance();
9627            Some(self.parse_expr(0)?)
9628        } else {
9629            None
9630        };
9631        // v7.39 (round 421, fixing round 420) — the SOURCE subquery's filter
9632        // and the TARGET-row filter are NOT the same predicate once a LEFT
9633        // join is involved:
9634        //   * inner / comma / PG's `FROM`: the ON predicate and the WHERE are
9635        //     one conjunction, and the whole thing filters target rows via
9636        //     EXISTS.
9637        //   * LEFT join: only the ON predicate belongs inside the source
9638        //     subquery. The WHERE still filters TARGET rows (with source
9639        //     columns read through the correlated subquery, which yields NULL
9640        //     for an unmatched row — exactly LEFT-join semantics).
9641        // Round 420 folded ON into WHERE unconditionally and then dropped the
9642        // outer filter for the LEFT case, so `UPDATE a LEFT JOIN b ON … SET …
9643        // WHERE a.id > 1` updated EVERY row.
9644        let sub_where = match (mysql_on.clone(), where_.clone()) {
9645            _ if mysql_outer => mysql_on.clone(),
9646            (Some(on), Some(w)) => Some(Expr::Binary {
9647                lhs: Box::new(on),
9648                op: crate::ast::BinOp::And,
9649                rhs: Box::new(w),
9650            }),
9651            (Some(on), None) => Some(on),
9652            (None, w) => w,
9653        };
9654        // v7.39 (round 413) — MySQL `UPDATE … [ORDER BY …] [LIMIT n]`. PG
9655        // has no such clause on UPDATE, so this is accepted only under the
9656        // MySQL dialect; a PG session's `UPDATE … ORDER BY …` still errors.
9657        let update_order_limit = self.parse_mysql_dml_order_limit("UPDATE")?;
9658        let mut returning = self.parse_optional_returning()?;
9659        // v7.39 (round 533) — kept for the engine, which can resolve the
9660        // UNQUALIFIED leaves this lowering has to leave alone.
9661        let from_sources = from_clause.as_ref().map(|fc| {
9662            alloc::boxed::Box::new(crate::ast::UpdateFromSources {
9663                from: fc.clone(),
9664                sub_where: sub_where.clone(),
9665            })
9666        });
9667        let (assignments, where_) = if let Some(fc) = from_clause {
9668            let names: Vec<String> = core::iter::once(&fc.primary)
9669                .chain(fc.joins.iter().map(|j| &j.table))
9670                .flat_map(|t| {
9671                    t.alias
9672                        .clone()
9673                        .into_iter()
9674                        .chain(core::iter::once(t.name.clone()))
9675                })
9676                .collect();
9677            let refs_list = |e: &Expr| -> bool {
9678                fn walk(e: &Expr, names: &[String]) -> bool {
9679                    match e {
9680                        Expr::Column(c) => c
9681                            .qualifier
9682                            .as_deref()
9683                            .is_some_and(|q| names.iter().any(|n| n.eq_ignore_ascii_case(q))),
9684                        Expr::Binary { lhs, rhs, .. } => walk(lhs, names) || walk(rhs, names),
9685                        Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, names),
9686                        Expr::FunctionCall { args, .. } => args.iter().any(|a| walk(a, names)),
9687                        Expr::Case {
9688                            operand,
9689                            branches,
9690                            else_branch,
9691                        } => {
9692                            operand.as_deref().is_some_and(|o| walk(o, names))
9693                                || branches
9694                                    .iter()
9695                                    .any(|(w, t)| walk(w, names) || walk(t, names))
9696                                || else_branch.as_deref().is_some_and(|el| walk(el, names))
9697                        }
9698                        _ => false,
9699                    }
9700                }
9701                walk(e, &names)
9702            };
9703            let sub_select = |items: Vec<SelectItem>| SelectStatement {
9704                locking: None,
9705                ctes: Vec::new(),
9706                distinct: false,
9707                distinct_on: Vec::new(),
9708                items,
9709                from: Some(fc.clone()),
9710                where_: sub_where.clone(),
9711                group_by: None,
9712                group_by_all: false,
9713                having: None,
9714                unions: Vec::new(),
9715                order_by: Vec::new(),
9716                limit: None,
9717                offset: None,
9718                limit_with_ties: false,
9719                window_check_exprs: Vec::new(),
9720            };
9721            // v7.37 D.30 — replace each FROM-qualified column *leaf* in the
9722            // assignment RHS with a correlated scalar subquery, instead of
9723            // wrapping the whole RHS. Wrapping the whole expr moved a target-
9724            // column reference (`SET v = v + u.bonus`, where `v` is the target
9725            // table's column) inside a subquery whose FROM only has the source
9726            // table, so the unqualified `v` resolved against the source and
9727            // errored ColumnNotFound. Leaving target columns in the outer UPDATE
9728            // context — where they belong — fixes it; only the source columns
9729            // (`u.bonus`) become subqueries. A whole-expr fallback covers
9730            // compound variants the leaf-walk doesn't decompose.
9731            let make_subq = |inner: Expr| {
9732                Expr::ScalarSubquery(Box::new(sub_select(alloc::vec![SelectItem::Expr {
9733                    expr: inner,
9734                    alias: None,
9735                }])))
9736            };
9737            let assignments = assignments
9738                .into_iter()
9739                .map(|(col, mut expr)| {
9740                    wrap_from_leaves(&mut expr, &names, &make_subq, &refs_list);
9741                    (col, expr)
9742                })
9743                .collect();
9744            let exists = Expr::Exists {
9745                subquery: Box::new(sub_select(alloc::vec![SelectItem::Expr {
9746                    expr: Expr::Literal(Literal::Integer(1)),
9747                    alias: None,
9748                }])),
9749                negated: false,
9750            };
9751            // v7.39 (round 241) — RETURNING may reference the FROM-list
9752            // tables too (`RETURNING emp.id, dept.name`); the same
9753            // leaf-to-correlated-subquery lowering the assignments get.
9754            // Without it the qualifier died at eval with "unknown table
9755            // qualifier". (RETURNING was parsed before this block — the
9756            // lowering is a pure AST transformation.)
9757            if let Some(items) = returning.as_mut() {
9758                for item in items.iter_mut() {
9759                    if let SelectItem::Expr { expr, .. } = item {
9760                        wrap_from_leaves(expr, &names, &make_subq, &refs_list);
9761                    }
9762                }
9763            }
9764            // v7.39 (round 420, corrected in 421) — a MySQL LEFT JOIN keeps
9765            // EVERY matching target row: it gets no EXISTS filter, but the
9766            // caller's WHERE still applies, with source columns read through
9767            // the correlated subquery (NULL when unmatched — LEFT-join
9768            // semantics). `sub_where` above already excluded the WHERE from
9769            // the source subquery for this case.
9770            if mysql_outer {
9771                let mut outer = where_;
9772                if let Some(w) = outer.as_mut() {
9773                    wrap_from_leaves(w, &names, &make_subq, &refs_list);
9774                }
9775                (assignments, outer)
9776            } else {
9777                (assignments, Some(exists))
9778            }
9779        } else {
9780            (assignments, where_)
9781        };
9782        Ok(Statement::Update(crate::ast::UpdateStatement {
9783            ctes: Vec::new(),
9784            table,
9785            only,
9786            alias,
9787            assignments,
9788            from_sources,
9789            where_,
9790            order_limit: update_order_limit,
9791            returning,
9792        }))
9793    }
9794
9795    /// v7.39 (round 432) — MySQL's `[ORDER BY …] [LIMIT n]` tail on a DML
9796    /// statement. UPDATE grew it in round 413 and DELETE in round 432; the
9797    /// clause and its meaning are identical, so both call this rather than
9798    /// keeping two copies that could disagree on, say, whether `LIMIT 0` is
9799    /// legal. PG has no such clause on either statement, so it is read only
9800    /// under the MySQL dialect — a PG session's `DELETE … ORDER BY …` still
9801    /// errors.
9802    ///
9803    /// `#[inline(never)]`: its locals would otherwise land on the statement-
9804    /// parsing recursion frame, which is what tipped the 512 KiB nesting
9805    /// stack in round 430.
9806    #[inline(never)]
9807    fn parse_mysql_dml_order_limit(
9808        &mut self,
9809        what: &str,
9810    ) -> Result<Option<alloc::boxed::Box<crate::ast::DmlOrderLimit>>, ParseError> {
9811        if !self.mysql_dialect {
9812            return Ok(None);
9813        }
9814        let order_by = self.parse_order_by_keys()?;
9815        let limit = if matches!(self.peek(), Token::Limit) {
9816            self.advance();
9817            let tok = self.advance();
9818            let Token::Integer(n) = tok else {
9819                return Err(self.err(alloc::format!(
9820                    "expected integer after {what} LIMIT, got {tok:?}"
9821                )));
9822            };
9823            // MySQL rejects the `LIMIT offset, count` form here — only a
9824            // single row count is legal on a DML statement.
9825            if matches!(self.peek(), Token::Comma) {
9826                return Err(self.err(alloc::format!(
9827                    "{what} LIMIT takes a row count, not an offset"
9828                )));
9829            }
9830            let n = u32::try_from(n)
9831                .map_err(|_| self.err(alloc::format!("{what} LIMIT out of range: {n}")))?;
9832            Some(n)
9833        } else {
9834            None
9835        };
9836        if order_by.is_empty() && limit.is_none() {
9837            return Ok(None);
9838        }
9839        Ok(Some(alloc::boxed::Box::new(crate::ast::DmlOrderLimit {
9840            order_by,
9841            limit,
9842        })))
9843    }
9844
9845    /// v4.4 `DELETE FROM <table> [WHERE cond]`. Caller already consumed
9846    /// the leading `DELETE` ident.
9847    fn parse_delete_after_keyword(&mut self) -> Result<Statement, ParseError> {
9848        // v7.39 (round 421) — MySQL's multi-table DELETE names its target(s)
9849        // BEFORE the FROM: `DELETE a FROM a JOIN b ON …`. (`DELETE FROM a
9850        // USING a, b WHERE …` — the third MySQL spelling — needs no special
9851        // parse here; it reaches the existing USING path with the target
9852        // repeated in the list, which the source-list peel below handles.)
9853        // More than one name is a multi-TARGET delete, which SPG does not
9854        // model; it is refused rather than half-applied.
9855        let mysql_pre_target: Option<String> =
9856            if self.mysql_dialect && !matches!(self.peek(), Token::From) {
9857                let first = self.expect_ident_like()?;
9858                if matches!(self.peek(), Token::Comma) {
9859                    return Err(self.err(alloc::format!(
9860                        "multi-table DELETE can only delete from one table; \
9861                     `DELETE {first}, …` names several"
9862                    )));
9863                }
9864                Some(first)
9865            } else {
9866                None
9867            };
9868        if !matches!(self.peek(), Token::From) {
9869            return Err(self.err(format!("expected FROM after DELETE, got {:?}", self.peek())));
9870        }
9871        self.advance();
9872        // v7.39 (round 646) — `DELETE FROM ONLY t`, same shape and same
9873        // lookahead as the UPDATE spelling.
9874        let only = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
9875            if s.eq_ignore_ascii_case("only"))
9876            && matches!(
9877                self.tokens.get(self.pos + 1),
9878                Some(Token::Ident(_) | Token::QuotedIdent(_))
9879            );
9880        if only {
9881            self.advance();
9882        }
9883        let table = self.expect_ident_like()?;
9884        // v7.39 (round 241) — `DELETE FROM t [AS] alias …`. The bare
9885        // spelling must not swallow the clause keywords that can follow
9886        // the target.
9887        let alias = if matches!(self.peek(), Token::As) {
9888            self.advance();
9889            Some(self.expect_ident_like()?)
9890        } else {
9891            match self.peek() {
9892                Token::Ident(s) | Token::QuotedIdent(s)
9893                    if !s.eq_ignore_ascii_case("using") && !s.eq_ignore_ascii_case("returning") =>
9894                {
9895                    let a = s.clone();
9896                    self.advance();
9897                    Some(a)
9898                }
9899                _ => None,
9900            }
9901        };
9902        // v7.39 (round 421) — MySQL's multi-table DELETE source list, read
9903        // through the SAME join grammar the FROM clause uses (see the
9904        // `advance()`-destroys-tokens note on `parse_from_joins`).
9905        let mut mysql_on: Option<Expr> = None;
9906        let mut mysql_outer = false;
9907        let mysql_using = if mysql_pre_target.is_some()
9908            && (matches!(self.peek(), Token::Comma) || self.peek_is_update_join_start())
9909        {
9910            let target_qual = alias.clone().unwrap_or_else(|| table.clone());
9911            let mut joins = self.parse_from_joins(&target_qual)?;
9912            if joins.is_empty() {
9913                return Err(self.err(alloc::string::String::from(
9914                    "multi-table DELETE needs at least one source table",
9915                )));
9916            }
9917            let head = joins.remove(0);
9918            mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9919            mysql_on = head.on;
9920            Some(FromClause {
9921                primary: head.table,
9922                joins,
9923            })
9924        } else {
9925            None
9926        };
9927        // The pre-FROM target must be the table the FROM names (or its
9928        // alias) — `DELETE b FROM a JOIN b …` would delete from a table that
9929        // is not the scan target.
9930        if let Some(t) = &mysql_pre_target {
9931            let names_target = t.eq_ignore_ascii_case(&table)
9932                || alias.as_deref().is_some_and(|a| a.eq_ignore_ascii_case(t));
9933            if !names_target {
9934                return Err(self.err(alloc::format!(
9935                    "DELETE target `{t}` is not the first table in the FROM clause ({table})"
9936                )));
9937            }
9938        }
9939        // `DELETE FROM t USING src [, …] WHERE cond` — PG's joined
9940        // delete. Same lowering as UPDATE … FROM: the WHERE
9941        // becomes EXISTS(SELECT 1 FROM src WHERE cond), driven per
9942        // target row by the correlated machinery.
9943        let using_clause = if let Some(fc) = mysql_using {
9944            Some(fc)
9945        } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
9946            self.advance();
9947            let mut fc = self.parse_from_clause()?;
9948            // v7.39 (round 421) — MySQL's `DELETE FROM a USING a, b WHERE …`
9949            // repeats the TARGET as the first USING entry (PG's spelling
9950            // lists only the extra sources). Peel it so the source subquery
9951            // does not re-scan — and shadow — the target table.
9952            let primary_is_target =
9953                fc.primary.name.eq_ignore_ascii_case(&table) && fc.primary.alias.is_none();
9954            if self.mysql_dialect && primary_is_target && !fc.joins.is_empty() {
9955                let head = fc.joins.remove(0);
9956                mysql_outer = matches!(head.kind, crate::ast::JoinKind::Left);
9957                mysql_on = head.on;
9958                fc = FromClause {
9959                    primary: head.table,
9960                    joins: fc.joins,
9961                };
9962            }
9963            Some(fc)
9964        } else {
9965            None
9966        };
9967        let where_ = if matches!(self.peek(), Token::Where) {
9968            self.advance();
9969            Some(self.parse_expr(0)?)
9970        } else {
9971            None
9972        };
9973        // v7.39 (round 432) — MySQL's `DELETE … [ORDER BY …] [LIMIT n]`,
9974        // read before RETURNING (MariaDB's own extension trails the LIMIT).
9975        let delete_order_limit = self.parse_mysql_dml_order_limit("DELETE")?;
9976        let mut returning = self.parse_optional_returning()?;
9977        let where_ = if let Some(fc) = using_clause {
9978            // v7.39 (round 241) — same RETURNING lowering as UPDATE…FROM:
9979            // a USING-table reference in RETURNING becomes a correlated
9980            // scalar subquery over the USING list.
9981            let names: Vec<String> = core::iter::once(&fc.primary)
9982                .chain(fc.joins.iter().map(|j| &j.table))
9983                .flat_map(|t| {
9984                    t.alias
9985                        .clone()
9986                        .into_iter()
9987                        .chain(core::iter::once(t.name.clone()))
9988                })
9989                .collect();
9990            // v7.39 (round 421) — same ON / WHERE split as UPDATE: a LEFT
9991            // join filters the SOURCE subquery on the ON predicate alone and
9992            // leaves the WHERE filtering TARGET rows (so the anti-join idiom
9993            // `LEFT JOIN b ON … WHERE b.id IS NULL` deletes the unmatched
9994            // rows); every other form folds ON and WHERE into one EXISTS.
9995            let sub_where = match (mysql_on.clone(), where_.clone()) {
9996                _ if mysql_outer => mysql_on.clone(),
9997                (Some(on), Some(w)) => Some(Expr::Binary {
9998                    lhs: Box::new(on),
9999                    op: crate::ast::BinOp::And,
10000                    rhs: Box::new(w),
10001                }),
10002                (Some(on), None) => Some(on),
10003                (None, w) => w,
10004            };
10005            let exists_where = sub_where.clone();
10006            let sub_fc = fc.clone();
10007            let make_subq = move |leaf: Expr| -> Expr {
10008                Expr::ScalarSubquery(Box::new(SelectStatement {
10009                    locking: None,
10010                    ctes: Vec::new(),
10011                    distinct: false,
10012                    distinct_on: Vec::new(),
10013                    items: alloc::vec![SelectItem::Expr {
10014                        expr: leaf,
10015                        alias: None,
10016                    }],
10017                    from: Some(sub_fc.clone()),
10018                    where_: sub_where.clone(),
10019                    group_by: None,
10020                    group_by_all: false,
10021                    having: None,
10022                    unions: Vec::new(),
10023                    order_by: Vec::new(),
10024                    limit: None,
10025                    offset: None,
10026                    limit_with_ties: false,
10027                    window_check_exprs: Vec::new(),
10028                }))
10029            };
10030            let refs = |e: &Expr| expr_refs_tables(e, &names);
10031            if let Some(items) = returning.as_mut() {
10032                for item in items.iter_mut() {
10033                    if let SelectItem::Expr { expr, .. } = item {
10034                        wrap_from_leaves(expr, &names, &make_subq, &refs);
10035                    }
10036                }
10037            }
10038            // A LEFT join deletes the target rows the WHERE selects, reading
10039            // source columns through the correlated subquery (NULL when
10040            // unmatched); no EXISTS row filter.
10041            if mysql_outer {
10042                let mut outer = where_;
10043                if let Some(w) = outer.as_mut() {
10044                    wrap_from_leaves(w, &names, &make_subq, &refs);
10045                }
10046                outer
10047            } else {
10048                Some(Expr::Exists {
10049                    subquery: Box::new(SelectStatement {
10050                        locking: None,
10051                        ctes: Vec::new(),
10052                        distinct: false,
10053                        distinct_on: Vec::new(),
10054                        items: alloc::vec![SelectItem::Expr {
10055                            expr: Expr::Literal(Literal::Integer(1)),
10056                            alias: None,
10057                        }],
10058                        from: Some(fc),
10059                        where_: exists_where,
10060                        group_by: None,
10061                        group_by_all: false,
10062                        having: None,
10063                        unions: Vec::new(),
10064                        order_by: Vec::new(),
10065                        limit: None,
10066                        offset: None,
10067                        limit_with_ties: false,
10068                        window_check_exprs: Vec::new(),
10069                    }),
10070                    negated: false,
10071                })
10072            }
10073        } else {
10074            where_
10075        };
10076        Ok(Statement::Delete(crate::ast::DeleteStatement {
10077            ctes: Vec::new(),
10078            table,
10079            only,
10080            alias,
10081            where_,
10082            order_limit: delete_order_limit,
10083            returning,
10084        }))
10085    }
10086
10087    /// v7.17.0 Phase 3.P0-42 — parse `MERGE INTO <target> [alias]
10088    /// USING <source> [alias] ON <expr> WHEN [NOT] MATCHED [AND
10089    /// <expr>] THEN <action> [WHEN …]` after the leading `MERGE`
10090    /// keyword. v7.17 surface:
10091    ///   * source: table reference (subquery source is a follow-up)
10092    ///   * actions: UPDATE SET / DELETE / DO NOTHING (matched);
10093    ///     INSERT (cols) VALUES (vals) / DO NOTHING (not matched)
10094    ///   * AND-conditioned WHEN clauses; clauses tried in declaration
10095    ///     order
10096    fn parse_merge_after_keyword(&mut self) -> Result<Statement, ParseError> {
10097        // INTO
10098        let is_into_kw = matches!(self.peek(), Token::Into)
10099            || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("into"));
10100        if !is_into_kw {
10101            return Err(self.err(format!("expected INTO after MERGE, got {:?}", self.peek())));
10102        }
10103        self.advance();
10104        let target = self.expect_ident_like()?;
10105        // Optional alias — bare ident before USING.
10106        let target_alias = match self.peek() {
10107            Token::Ident(s) | Token::QuotedIdent(s) if !s.eq_ignore_ascii_case("using") => {
10108                Some(self.expect_ident_like()?)
10109            }
10110            _ => None,
10111        };
10112        // USING
10113        let is_using_kw = matches!(
10114            self.peek(),
10115            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using")
10116        );
10117        if !is_using_kw {
10118            return Err(self.err(format!(
10119                "expected USING after MERGE INTO target, got {:?}",
10120                self.peek()
10121            )));
10122        }
10123        self.advance();
10124        // v7.37 D.44 — `USING (SELECT …) alias` subquery source, or `USING
10125        // <table> [alias]`. PG requires an alias after a subquery source.
10126        let (source, source_select) = if matches!(self.peek(), Token::LParen) {
10127            self.advance(); // (
10128            // v7.39 (round 768, F31-D5) — `USING (VALUES …)`: the same
10129            // constant-SELECT lowering the derived-table parser uses
10130            // (PG deletes through this form; it was a parse error).
10131            let inner = if matches!(self.peek(), Token::Values) {
10132                self.advance(); // VALUES
10133                Statement::Select(self.parse_values_rows_body()?)
10134            } else {
10135                self.parse_select_stmt()?
10136            };
10137            match self.advance() {
10138                Token::RParen => {}
10139                other => {
10140                    return Err(self.err(format!(
10141                        "expected ')' after MERGE USING subquery, got {other:?}"
10142                    )));
10143                }
10144            }
10145            let Statement::Select(sub) = inner else {
10146                return Err(self.err("MERGE USING subquery must be a SELECT".into()));
10147            };
10148            (String::new(), Some(Box::new(sub)))
10149        } else {
10150            (self.expect_ident_like()?, None)
10151        };
10152        let source_alias = match self.peek() {
10153            Token::Ident(s) | Token::QuotedIdent(s)
10154                if !s.eq_ignore_ascii_case("on") && !s.eq_ignore_ascii_case("as") =>
10155            {
10156                Some(self.expect_ident_like()?)
10157            }
10158            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("as") => {
10159                self.advance(); // AS
10160                Some(self.expect_ident_like()?)
10161            }
10162            _ => None,
10163        };
10164        // v7.39 (round 768, F31-D5) — optional positional column-alias
10165        // list after the source alias (`s(id, v)`).
10166        let mut source_column_aliases: Vec<String> = Vec::new();
10167        if source_alias.is_some() && matches!(self.peek(), Token::LParen) {
10168            self.advance();
10169            loop {
10170                source_column_aliases.push(self.expect_ident_like()?);
10171                match self.peek() {
10172                    Token::Comma => {
10173                        self.advance();
10174                    }
10175                    Token::RParen => {
10176                        self.advance();
10177                        break;
10178                    }
10179                    other => {
10180                        return Err(self.err(format!(
10181                            "expected ',' or ')' in MERGE source column list, got {other:?}"
10182                        )));
10183                    }
10184                }
10185            }
10186        }
10187        if source_select.is_some() && source_alias.is_none() {
10188            return Err(self.err("MERGE USING (subquery) requires an alias".into()));
10189        }
10190        // ON
10191        if !matches!(self.peek(), Token::On) {
10192            return Err(self.err(format!(
10193                "expected ON after MERGE … USING source, got {:?}",
10194                self.peek()
10195            )));
10196        }
10197        self.advance();
10198        let on = self.parse_expr(0)?;
10199        // One or more WHEN clauses.
10200        let mut clauses: Vec<crate::ast::MergeWhenClause> = Vec::new();
10201        loop {
10202            let is_when_kw = matches!(
10203                self.peek(),
10204                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("when")
10205            );
10206            if !is_when_kw {
10207                break;
10208            }
10209            self.advance(); // WHEN
10210            // [NOT] MATCHED
10211            let matched = if matches!(self.peek(), Token::Not) {
10212                self.advance();
10213                crate::ast::MergeMatched::NotMatched
10214            } else {
10215                crate::ast::MergeMatched::Matched
10216            };
10217            let is_matched_kw = matches!(
10218                self.peek(),
10219                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("matched")
10220            );
10221            if !is_matched_kw {
10222                return Err(self.err(format!(
10223                    "expected MATCHED in WHEN clause, got {:?}",
10224                    self.peek()
10225                )));
10226            }
10227            self.advance();
10228            // v7.39 (round 146, PG17) — `NOT MATCHED [BY TARGET | BY SOURCE]`.
10229            // BY TARGET is the default (a synonym); BY SOURCE flips the clause
10230            // to fire for target rows no source row matches.
10231            let mut matched = matched;
10232            if matches!(matched, crate::ast::MergeMatched::NotMatched) && self.peek_is_by() {
10233                self.advance();
10234                match self.peek() {
10235                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("source") => {
10236                        self.advance();
10237                        matched = crate::ast::MergeMatched::NotMatchedBySource;
10238                    }
10239                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("target") => {
10240                        self.advance();
10241                    }
10242                    other => {
10243                        return Err(self.err(format!(
10244                            "expected SOURCE or TARGET after NOT MATCHED BY, got {other:?}"
10245                        )));
10246                    }
10247                }
10248            }
10249            // Optional AND <expr>
10250            let condition = if matches!(self.peek(), Token::And) {
10251                self.advance();
10252                Some(self.parse_expr(0)?)
10253            } else {
10254                None
10255            };
10256            // THEN
10257            let is_then_kw = matches!(
10258                self.peek(),
10259                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("then")
10260            );
10261            if !is_then_kw {
10262                return Err(self.err(format!(
10263                    "expected THEN in WHEN clause, got {:?}",
10264                    self.peek()
10265                )));
10266            }
10267            self.advance();
10268            // Action: INSERT / UPDATE / DELETE / DO NOTHING
10269            let action = match self.peek().clone() {
10270                Token::Insert => {
10271                    self.advance();
10272                    // v7.39 (read01 round 123) — the `(cols)` list is OPTIONAL,
10273                    // exactly like a plain INSERT: `WHEN NOT MATCHED THEN INSERT
10274                    // VALUES (…)` omits it and fills every column in declaration
10275                    // order. PG accepts this; SPG used to require the list.
10276                    let mut columns: Vec<String> = Vec::new();
10277                    if matches!(self.peek(), Token::LParen) {
10278                        self.advance();
10279                        loop {
10280                            columns.push(self.expect_ident_like()?);
10281                            if matches!(self.peek(), Token::Comma) {
10282                                self.advance();
10283                                continue;
10284                            }
10285                            break;
10286                        }
10287                        if !matches!(self.peek(), Token::RParen) {
10288                            return Err(self.err(format!(
10289                                "expected ')' after INSERT column list, got {:?}",
10290                                self.peek()
10291                            )));
10292                        }
10293                        self.advance();
10294                    }
10295                    // VALUES (...)
10296                    if !matches!(self.peek(), Token::Values) {
10297                        return Err(self.err(format!(
10298                            "expected VALUES in MERGE INSERT, got {:?}",
10299                            self.peek()
10300                        )));
10301                    }
10302                    self.advance();
10303                    if !matches!(self.peek(), Token::LParen) {
10304                        return Err(self.err(format!(
10305                            "expected '(' after VALUES in MERGE INSERT, got {:?}",
10306                            self.peek()
10307                        )));
10308                    }
10309                    self.advance();
10310                    let mut values: Vec<crate::ast::Expr> = Vec::new();
10311                    loop {
10312                        values.push(self.parse_expr(0)?);
10313                        if matches!(self.peek(), Token::Comma) {
10314                            self.advance();
10315                            continue;
10316                        }
10317                        break;
10318                    }
10319                    if !matches!(self.peek(), Token::RParen) {
10320                        return Err(self.err(format!(
10321                            "expected ')' after MERGE INSERT values, got {:?}",
10322                            self.peek()
10323                        )));
10324                    }
10325                    self.advance();
10326                    // Empty column list = positional into every column, so the
10327                    // count is checked against the table arity at execution.
10328                    if !columns.is_empty() && columns.len() != values.len() {
10329                        return Err(self.err(format!(
10330                            "MERGE INSERT column count ({}) ≠ value count ({})",
10331                            columns.len(),
10332                            values.len()
10333                        )));
10334                    }
10335                    crate::ast::MergeAction::Insert { columns, values }
10336                }
10337                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
10338                    self.advance();
10339                    // SET
10340                    let is_set_kw = matches!(
10341                        self.peek(),
10342                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set")
10343                    );
10344                    if !is_set_kw {
10345                        return Err(self.err(format!(
10346                            "expected SET after UPDATE in MERGE, got {:?}",
10347                            self.peek()
10348                        )));
10349                    }
10350                    self.advance();
10351                    let mut assignments: Vec<(String, crate::ast::Expr)> = Vec::new();
10352                    loop {
10353                        let col = self.expect_ident_like()?;
10354                        if !matches!(self.peek(), Token::Eq) {
10355                            return Err(self.err(format!(
10356                                "expected '=' in MERGE UPDATE assignment, got {:?}",
10357                                self.peek()
10358                            )));
10359                        }
10360                        self.advance();
10361                        let expr = self.parse_expr(0)?;
10362                        assignments.push((col, expr));
10363                        if matches!(self.peek(), Token::Comma) {
10364                            self.advance();
10365                            continue;
10366                        }
10367                        break;
10368                    }
10369                    crate::ast::MergeAction::Update { assignments }
10370                }
10371                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete") => {
10372                    self.advance();
10373                    crate::ast::MergeAction::Delete
10374                }
10375                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {
10376                    self.advance();
10377                    let is_nothing_kw = matches!(
10378                        self.peek(),
10379                        Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing")
10380                    );
10381                    if !is_nothing_kw {
10382                        return Err(self.err(format!(
10383                            "expected NOTHING after DO in MERGE clause, got {:?}",
10384                            self.peek()
10385                        )));
10386                    }
10387                    self.advance();
10388                    crate::ast::MergeAction::DoNothing
10389                }
10390                other => {
10391                    return Err(self.err(format!(
10392                        "expected INSERT / UPDATE / DELETE / DO NOTHING in MERGE clause, got {other:?}"
10393                    )));
10394                }
10395            };
10396            // PG's grammar simply has no INSERT production under BY SOURCE
10397            // (a target row already exists there) — same syntax error.
10398            if matches!(matched, crate::ast::MergeMatched::NotMatchedBySource)
10399                && matches!(action, crate::ast::MergeAction::Insert { .. })
10400            {
10401                return Err(self.err(String::from("syntax error at or near \"INSERT\"")));
10402            }
10403            clauses.push(crate::ast::MergeWhenClause {
10404                matched,
10405                condition,
10406                action,
10407            });
10408        }
10409        if clauses.is_empty() {
10410            return Err(self.err(String::from("MERGE requires at least one WHEN clause")));
10411        }
10412        // v7.38 (read01 U-merge) — PG rejects a WHEN clause that follows an
10413        // unconditional (no `AND`) WHEN of the same match kind: it could
10414        // never fire. Check per match kind in clause order.
10415        let mut seen_unconditional_matched = false;
10416        let mut seen_unconditional_not_matched = false;
10417        let mut seen_unconditional_by_source = false;
10418        for c in &clauses {
10419            let seen = match c.matched {
10420                crate::ast::MergeMatched::Matched => &mut seen_unconditional_matched,
10421                crate::ast::MergeMatched::NotMatched => &mut seen_unconditional_not_matched,
10422                crate::ast::MergeMatched::NotMatchedBySource => &mut seen_unconditional_by_source,
10423            };
10424            if *seen {
10425                return Err(self.err(String::from(
10426                    "unreachable WHEN clause specified after unconditional WHEN clause",
10427                )));
10428            }
10429            if c.condition.is_none() {
10430                *seen = true;
10431            }
10432        }
10433        // v7.39 (round 130) — optional trailing `RETURNING <projection>` (PG17+).
10434        let returning = self.parse_optional_returning()?;
10435        Ok(Statement::Merge(crate::ast::MergeStatement {
10436            // Attached by `parse_with_cte_then_select` when the MERGE
10437            // heads a WITH clause (round 149).
10438            ctes: Vec::new(),
10439            target,
10440            target_alias,
10441            source,
10442            source_alias,
10443            source_select,
10444            source_column_aliases,
10445            on,
10446            clauses,
10447            returning,
10448        }))
10449    }
10450
10451    /// v7.9.4 — parse the optional trailing `RETURNING <projection>`
10452    /// clause on INSERT / UPDATE / DELETE. Same projection grammar
10453    /// as SELECT, so `RETURNING *`, `RETURNING col`,
10454    /// `RETURNING expr AS alias`, and `RETURNING a, b, c` all work.
10455    fn parse_optional_returning(
10456        &mut self,
10457    ) -> Result<Option<Vec<crate::ast::SelectItem>>, ParseError> {
10458        let is_returning_kw = matches!(
10459            self.peek(),
10460            Token::Ident(s) if s.eq_ignore_ascii_case("returning")
10461        );
10462        if !is_returning_kw {
10463            return Ok(None);
10464        }
10465        self.advance();
10466        let mut items = Vec::new();
10467        loop {
10468            items.push(self.parse_select_item()?);
10469            if matches!(self.peek(), Token::Comma) {
10470                self.advance();
10471                continue;
10472            }
10473            break;
10474        }
10475        Ok(Some(items))
10476    }
10477
10478    /// v6.0.4 — parse the tail of an ALTER statement after the
10479    /// leading `ALTER` keyword has been consumed. Only one form is
10480    /// supported in v6.0.4:
10481    ///
10482    /// ```text
10483    /// ALTER INDEX <name> REBUILD [WITH (encoding = <enc>)]
10484    /// ```
10485    fn parse_alter_after_keyword(&mut self) -> Result<Statement, ParseError> {
10486        // ALTER INDEX <name> ... | ALTER TABLE <name> SET hot_tier_bytes = <n>
10487        // v7.14.0 — `ALTER TABLE ONLY` modifier (PG partition-
10488        // exclusion) is accepted by stripping the `ONLY` keyword
10489        // before the table parse.
10490        // v7.14.0 — `ALTER SEQUENCE / ALTER VIEW / ALTER OWNER`
10491        // and the long PG-dump tail are accepted as no-ops.
10492        match self.advance() {
10493            Token::Index => {}
10494            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("index") => {}
10495            // v6.7.2 — ALTER TABLE t SET hot_tier_bytes = X
10496            // v7.14.0 — ALTER TABLE ONLY t … strip the `ONLY`.
10497            Token::Table => {
10498                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10499                    self.advance();
10500                }
10501                return self.parse_alter_table_after_keyword();
10502            }
10503            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("policy") => {
10504                return self.parse_alter_policy_after_keyword();
10505            }
10506            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("table") => {
10507                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("only")) {
10508                    self.advance();
10509                }
10510                return self.parse_alter_table_after_keyword();
10511            }
10512            // v7.17.0 — ALTER SEQUENCE name <options>. Moved out
10513            // of the silent-noop tail.
10514            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("sequence") => {
10515                return self.parse_alter_sequence_after_keyword();
10516            }
10517            // v7.37 D.55 — ALTER TYPE name ADD VALUE [IF NOT EXISTS] 'label'
10518            // [{BEFORE | AFTER} 'existing']. Real enum evolution; other ALTER
10519            // TYPE forms (RENAME / OWNER / SET SCHEMA) still no-op below.
10520            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("type") => {
10521                // NB: the match arm consumed `TYPE` via self.advance(); the
10522                // cursor is now at the type name — do NOT advance again.
10523                let type_name = self.expect_ident_like()?;
10524                let is_add_value = matches!(self.peek(), Token::Ident(a) if a.eq_ignore_ascii_case("add"))
10525                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(v)) if v.eq_ignore_ascii_case("value"));
10526                if is_add_value {
10527                    self.advance(); // ADD
10528                    self.advance(); // VALUE
10529                    // `IF NOT EXISTS` — NOT lexes as the keyword `Token::Not`,
10530                    // IF/EXISTS as identifiers.
10531                    let if_not_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"))
10532                    {
10533                        let n1 = self.tokens.get(self.pos + 1);
10534                        let n2 = self.tokens.get(self.pos + 2);
10535                        if matches!(n1, Some(Token::Not))
10536                            && matches!(n2, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists"))
10537                        {
10538                            self.advance();
10539                            self.advance();
10540                            self.advance();
10541                            true
10542                        } else {
10543                            false
10544                        }
10545                    } else {
10546                        false
10547                    };
10548                    let label = self.expect_string_literal()?;
10549                    let position = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before") || s.eq_ignore_ascii_case("after"))
10550                    {
10551                        let is_before = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("before"));
10552                        self.advance();
10553                        let anchor = self.expect_string_literal()?;
10554                        Some((is_before, anchor))
10555                    } else {
10556                        None
10557                    };
10558                    return Ok(Statement::AlterTypeAddValue {
10559                        type_name,
10560                        label,
10561                        if_not_exists,
10562                        position,
10563                    });
10564                }
10565                // v7.39 (read01 round 49) — `RENAME VALUE 'old' TO 'new'`.
10566                // Used to fall into the no-op tail below: accepted, silently
10567                // ignored. `RENAME TO <newtype>` keeps falling through.
10568                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename"))
10569                    && matches!(
10570                        self.tokens.get(self.pos + 1),
10571                        Some(Token::Ident(s)) if s.eq_ignore_ascii_case("value")
10572                    )
10573                {
10574                    self.advance(); // RENAME
10575                    self.advance(); // VALUE
10576                    let old = self.expect_string_literal()?;
10577                    if matches!(self.peek(), Token::To) {
10578                        self.advance();
10579                    } else {
10580                        self.expect_keyword_ident("to")?;
10581                    }
10582                    let new = self.expect_string_literal()?;
10583                    return Ok(Statement::AlterTypeRenameValue {
10584                        type_name,
10585                        old,
10586                        new,
10587                    });
10588                }
10589                // Other ALTER TYPE forms — the ACTION stays a no-op
10590                // (pg_dump tail), but v7.39 (round 708) the NAME is
10591                // validated: `ALTER TYPE nosuch RENAME TO x` reported
10592                // success for a type that does not exist.
10593                self.consume_until_statement_boundary();
10594                return Ok(Statement::ValidateOnly {
10595                    kind: crate::ast::ValidateOnlyKind::TypeName,
10596                    names: alloc::vec![type_name],
10597                });
10598            }
10599            // v7.14.0 — ALTER VIEW / ALTER FUNCTION /
10600            // ALTER DOMAIN / ALTER DATABASE / ALTER USER / ALTER
10601            // ROLE / ALTER SCHEMA / ALTER OWNER / ALTER DEFAULT
10602            // PRIVILEGES — accept as no-op so pg_dump's tail loads.
10603            // v7.17.0 NOTE: ALTER SEQUENCE moved out (above).
10604            // v7.39 (round 260) — ALTER DOMAIN is REAL now, so it leaves the
10605            // pg_dump no-op list below: every form used to report success
10606            // and change nothing, which is worse than refusing outright
10607            // (a migration dropping a constraint kept rejecting data).
10608            // NOTE: the enclosing `match self.advance()` already consumed
10609            // the DOMAIN keyword, so the name is next.
10610            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("domain") => {
10611                return self.parse_alter_domain_after_keyword();
10612            }
10613            // v7.39 (round 547) — `ALTER ROLE|USER <r> [IN DATABASE <d>]
10614            // SET|RESET …` and `ALTER DATABASE <d> SET|RESET …`. These
10615            // used to fall into the pg_dump no-op tail below, so a DBA
10616            // setting a per-role default was told it worked and nothing
10617            // happened. Intercepted here, BEFORE that tail.
10618            // v7.39 (round 695) — `ALTER SYSTEM SET <name> = …` / `RESET
10619            // <name>` / `RESET ALL`. Same reason the ROLE / DATABASE
10620            // interception below exists: swallowed with the no-op tail, an
10621            // unknown parameter name was ACCEPTED where PG18 answers
10622            // `unrecognized configuration parameter`. SPG applies nothing
10623            // either way — there is no postgresql.auto.conf — but it now
10624            // says so about a name it does not know.
10625            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("system") => {
10626                // NOTE: the scrutinee is `self.advance()`, so SYSTEM is
10627                // already consumed here. An extra advance eats the SET and
10628                // the parameter name is never seen — which is exactly the
10629                // bug a panic in this branch disproved: the branch WAS on
10630                // the path, the reading of it was wrong.
10631                let mut parameter = None;
10632                // SET <name> … | RESET <name> | RESET ALL
10633                if matches!(self.peek(), Token::Ident(k)
10634                    if k.eq_ignore_ascii_case("set") || k.eq_ignore_ascii_case("reset"))
10635                {
10636                    self.advance();
10637                    if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
10638                        && !n.eq_ignore_ascii_case("all")
10639                    {
10640                        self.advance();
10641                        // A dotted GUC (`plpgsql.check_asserts`) is two
10642                        // tokens; keep the whole name.
10643                        let mut full = n;
10644                        while matches!(self.peek(), Token::Dot) {
10645                            self.advance();
10646                            if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
10647                                full.push('.');
10648                                full.push_str(&t);
10649                            }
10650                        }
10651                        parameter = Some(full);
10652                    }
10653                }
10654                self.consume_until_statement_boundary();
10655                return Ok(Statement::AlterSystem { parameter });
10656            }
10657            Token::Ident(s) | Token::QuotedIdent(s)
10658                if matches!(
10659                    s.to_ascii_lowercase().as_str(),
10660                    "role" | "user" | "database"
10661                ) && self.peeks_db_role_setting() =>
10662            {
10663                let is_database = s.eq_ignore_ascii_case("database");
10664                return self.parse_db_role_setting(is_database);
10665            }
10666            // v7.39 (round 708) — `ALTER ROLE|USER <name> [WITH attrs…]`
10667            // (the non-SET forms; SET/RESET took the branch above). The
10668            // attributes still no-op — recorded, and the ignored PASSWORD
10669            // is ledgered as its own follow-up — but the ROLE is validated:
10670            // any name was accepted for a role that does not exist.
10671            Token::Ident(s) | Token::QuotedIdent(s)
10672                if s.eq_ignore_ascii_case("role") || s.eq_ignore_ascii_case("user") =>
10673            {
10674                // NB: the enclosing `match self.advance()` already consumed
10675                // ROLE/USER — the round-695 trap, hit again in this round's
10676                // first draft (the name was eaten and WITH parsed as the
10677                // role). The cursor is at the name.
10678                let name = self.expect_ident_or_string()?;
10679                // v7.39 (round 750) — scan the attribute tail for
10680                // PASSWORD. Everything else stays a recorded no-op, but
10681                // a dropped credential rotation is a SECURITY bug:
10682                // `ALTER USER x PASSWORD 'new'` answered ALTER ROLE and
10683                // changed nothing, so the old password kept working.
10684                // ENCRYPTED/UNENCRYPTED are PG-noise prefixes; `PASSWORD
10685                // NULL` clears the credential.
10686                let mut password: Option<Option<String>> = None;
10687                loop {
10688                    match self.peek() {
10689                        Token::Semicolon | Token::Eof => break,
10690                        Token::Ident(w) if w.eq_ignore_ascii_case("password") => {
10691                            self.advance();
10692                            match self.advance() {
10693                                Token::String(p) => password = Some(Some(p)),
10694                                Token::Null => password = Some(None),
10695                                Token::Ident(n) if n.eq_ignore_ascii_case("null") => {
10696                                    password = Some(None);
10697                                }
10698                                other => {
10699                                    return Err(self.err(alloc::format!(
10700                                        "expected password string or NULL after PASSWORD, got {other:?}"
10701                                    )));
10702                                }
10703                            }
10704                        }
10705                        _ => {
10706                            self.advance();
10707                        }
10708                    }
10709                }
10710                if name.eq_ignore_ascii_case("all") {
10711                    // `ALTER ROLE ALL …` names every role; nothing to check.
10712                    return Ok(Statement::Empty);
10713                }
10714                if let Some(pw) = password {
10715                    return Ok(Statement::AlterRolePassword { name, password: pw });
10716                }
10717                return Ok(Statement::ValidateOnly {
10718                    kind: crate::ast::ValidateOnlyKind::RoleName,
10719                    names: alloc::vec![name],
10720                });
10721            }
10722            // v7.39 (round 709) — ALTER COLLATION / TEXT SEARCH
10723            // CONFIGURATION / EVENT TRIGGER / LARGE OBJECT leave the no-op
10724            // list far enough to validate the NAME; the actions still no-op.
10725            // (TEXT SEARCH DICTIONARY / PARSER / TEMPLATE stay noise: SPG
10726            // models none of them and their dumps are rare.)
10727            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("collation") => {
10728                let name = self.expect_ident_or_string()?;
10729                self.consume_until_statement_boundary();
10730                return Ok(Statement::ValidateOnly {
10731                    kind: crate::ast::ValidateOnlyKind::CollationName,
10732                    names: alloc::vec![name],
10733                });
10734            }
10735            Token::Ident(s) | Token::QuotedIdent(s)
10736                if s.eq_ignore_ascii_case("text")
10737                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("search"))
10738                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("configuration")) =>
10739            {
10740                self.advance(); // SEARCH
10741                self.advance(); // CONFIGURATION
10742                let name = self.expect_ident_like()?;
10743                self.consume_until_statement_boundary();
10744                return Ok(Statement::ValidateOnly {
10745                    kind: crate::ast::ValidateOnlyKind::TsConfigName,
10746                    names: alloc::vec![name],
10747                });
10748            }
10749            Token::Ident(s) | Token::QuotedIdent(s)
10750                if s.eq_ignore_ascii_case("event")
10751                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("trigger")) =>
10752            {
10753                self.advance(); // TRIGGER
10754                let name = self.expect_ident_like()?;
10755                self.consume_until_statement_boundary();
10756                return Ok(Statement::ValidateOnly {
10757                    kind: crate::ast::ValidateOnlyKind::EventTriggerName,
10758                    names: alloc::vec![name],
10759                });
10760            }
10761            Token::Ident(s) | Token::QuotedIdent(s)
10762                if s.eq_ignore_ascii_case("large")
10763                    && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("object")) =>
10764            {
10765                self.advance(); // OBJECT
10766                let oid = match self.advance() {
10767                    Token::Integer(n) => alloc::format!("{n}"),
10768                    other => {
10769                        return Err(
10770                            self.err(alloc::format!("expected large object oid, got {other:?}"))
10771                        );
10772                    }
10773                };
10774                self.consume_until_statement_boundary();
10775                return Ok(Statement::ValidateOnly {
10776                    kind: crate::ast::ValidateOnlyKind::LargeObjectOid,
10777                    names: alloc::vec![oid],
10778                });
10779            }
10780            // v7.39 (round 708) — `ALTER AGGREGATE name(args) …`. Same
10781            // argument-list parse as DROP AGGREGATE (round 707); the
10782            // action no-ops, the existence check is real.
10783            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("aggregate") => {
10784                // Same round-695 trap as above: AGGREGATE is already
10785                // consumed; the cursor is at the name.
10786                let name = self.expect_ident_like()?;
10787                let mut names = alloc::vec![name];
10788                if matches!(self.peek(), Token::LParen) {
10789                    self.advance();
10790                    loop {
10791                        match self.peek().clone() {
10792                            Token::RParen => {
10793                                self.advance();
10794                                break;
10795                            }
10796                            Token::Star => {
10797                                self.advance();
10798                                names.push(String::from("*"));
10799                            }
10800                            Token::Comma => {
10801                                self.advance();
10802                            }
10803                            _ => {
10804                                let mut t = self.expect_ident_like()?;
10805                                while let Token::Ident(nx) = self.peek() {
10806                                    let nx = nx.clone();
10807                                    self.advance();
10808                                    t.push(' ');
10809                                    t.push_str(&nx);
10810                                }
10811                                names.push(t);
10812                            }
10813                        }
10814                    }
10815                }
10816                self.consume_until_statement_boundary();
10817                return Ok(Statement::ValidateOnly {
10818                    kind: crate::ast::ValidateOnlyKind::AggregateName,
10819                    names,
10820                });
10821            }
10822            Token::Ident(s) | Token::QuotedIdent(s)
10823                if matches!(
10824                    s.to_ascii_lowercase().as_str(),
10825                    "view"
10826                        | "function"
10827                        | "database"
10828                        | "schema"
10829                        | "owner"
10830                        | "default"
10831                        | "extension"
10832                        | "materialized"
10833                        | "publication"
10834                        | "subscription"
10835                        // v7.37.17 (17.6 siblings) — additional ALTER
10836                        // targets pg_dump / pg_dumpall / operator DB
10837                        // migration scripts commonly emit. SPG has
10838                        // no matching machinery for any of these; the
10839                        // parser accepts + Empty-returns so pg_dump
10840                        // tail statements don't stall.
10841                        | "tablespace"
10842                        | "language"
10843                        | "operator"
10844                        | "conversion"
10845                        | "statistics"
10846                        | "server"
10847                        | "foreign"
10848                        // `text` stays for TEXT SEARCH DICTIONARY / PARSER
10849                        // / TEMPLATE (CONFIGURATION intercepted above).
10850                        | "text"
10851                ) =>
10852            {
10853                self.consume_until_statement_boundary();
10854                return Ok(Statement::Empty);
10855            }
10856            other => {
10857                return Err(self.err(format!(
10858                    "expected INDEX / TABLE / SEQUENCE / VIEW / FUNCTION / TYPE / OWNER / etc \
10859                     after ALTER, got {other:?}"
10860                )));
10861            }
10862        }
10863        // v7.16.2 — optional `IF EXISTS` after ALTER INDEX
10864        // (mailrs migrate-042 ships these). The presence of an
10865        // IF EXISTS makes the subsequent name lookup tolerate
10866        // a missing index — engine returns CommandOk no-op.
10867        let if_exists = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
10868            let next = self.tokens.get(self.pos + 1);
10869            if matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
10870                self.advance();
10871                self.advance();
10872                true
10873            } else {
10874                false
10875            }
10876        } else {
10877            false
10878        };
10879        let name = self.expect_ident_like()?;
10880        // v7.16.2 — RENAME TO new_name shape (mailrs migrate-042).
10881        // Detect BEFORE the REBUILD path so the existing REBUILD
10882        // arm stays untouched.
10883        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rename")) {
10884            self.advance();
10885            if matches!(self.peek(), Token::To) {
10886                self.advance();
10887            } else {
10888                self.expect_keyword_ident("to")?;
10889            }
10890            let new = self.expect_ident_like()?;
10891            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10892                name,
10893                target: crate::ast::AlterIndexTarget::Rename { new, if_exists },
10894            }));
10895        }
10896        // v7.39 (round 710) — SET ( … ) / RESET ( … ) storage parameters.
10897        // A syntax error before; the index is validated, the params no-op.
10898        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("reset"))
10899            || (matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("set"))
10900                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)))
10901        {
10902            self.consume_until_statement_boundary();
10903            return Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10904                name,
10905                target: crate::ast::AlterIndexTarget::StorageParams,
10906            }));
10907        }
10908        // REBUILD
10909        self.expect_keyword_ident("rebuild")?;
10910        // Optional: WITH (encoding = <enc>)
10911        let encoding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
10912            self.advance();
10913            if !matches!(self.peek(), Token::LParen) {
10914                return Err(self.err(format!(
10915                    "expected '(' after WITH in ALTER INDEX REBUILD, got {:?}",
10916                    self.peek()
10917                )));
10918            }
10919            self.advance();
10920            self.expect_keyword_ident("encoding")?;
10921            if !matches!(self.peek(), Token::Eq) {
10922                return Err(self.err(format!(
10923                    "expected '=' after encoding in ALTER INDEX REBUILD, got {:?}",
10924                    self.peek()
10925                )));
10926            }
10927            self.advance();
10928            let enc_ident = match self.advance() {
10929                Token::Ident(s) | Token::QuotedIdent(s) => s,
10930                other => {
10931                    return Err(self.err(format!("expected encoding name after =, got {other:?}")));
10932                }
10933            };
10934            let enc = match enc_ident.to_ascii_lowercase().as_str() {
10935                "f32" => VecEncoding::F32,
10936                "sq8" => VecEncoding::Sq8,
10937                "half" => VecEncoding::F16,
10938                other => {
10939                    return Err(self.err(format!(
10940                        "unknown vector encoding {other:?} in ALTER INDEX REBUILD; supported: F32, SQ8, HALF"
10941                    )));
10942                }
10943            };
10944            if !matches!(self.peek(), Token::RParen) {
10945                return Err(self.err(format!(
10946                    "expected ')' after encoding value, got {:?}",
10947                    self.peek()
10948                )));
10949            }
10950            self.advance();
10951            Some(enc)
10952        } else {
10953            None
10954        };
10955        Ok(Statement::AlterIndex(crate::ast::AlterIndexStatement {
10956            name,
10957            target: crate::ast::AlterIndexTarget::Rebuild { encoding },
10958        }))
10959    }
10960
10961    /// v6.7.2 — `ALTER TABLE <name> SET hot_tier_bytes = <n>`. The
10962    /// only `SET` form currently supported; future v6.7.x can add
10963    /// more SET subjects without changing the dispatch shape.
10964    /// v7.13.2 — mailrs round-6 S1: accepts comma-separated
10965    /// subactions. Single-subaction shape stays a 1-element vec.
10966    fn parse_alter_table_after_keyword(&mut self) -> Result<Statement, ParseError> {
10967        let table_name = self.expect_ident_like()?;
10968        let mut targets: Vec<crate::ast::AlterTableTarget> = Vec::new();
10969        loop {
10970            let subaction = self.parse_alter_table_subaction()?;
10971            // ADD COLUMN with inline REFERENCES emits both an
10972            // AddColumn and an AddForeignKey subaction; the
10973            // helper returns 1 or 2 items.
10974            targets.extend(subaction);
10975            if matches!(self.peek(), Token::Comma) {
10976                self.advance();
10977                continue;
10978            }
10979            break;
10980        }
10981        Ok(Statement::AlterTable(crate::ast::AlterTableStatement {
10982            name: table_name,
10983            targets,
10984        }))
10985    }
10986
10987    /// Parse one ALTER TABLE subaction. Returns a Vec because
10988    /// inline `REFERENCES` on `ADD COLUMN` produces both an
10989    /// AddColumn and an AddForeignKey entry (mailrs round-6 S3).
10990    /// v7.39.9 — MySQL's `FIRST` / `AFTER <col>` trailer on ADD /
10991    /// MODIFY / CHANGE COLUMN. Absent is the PostgreSQL form, which
10992    /// appends.
10993    fn parse_column_position(&mut self) -> Option<crate::ast::ColumnPosition> {
10994        match self.peek() {
10995            Token::Ident(s) if s.eq_ignore_ascii_case("first") => {
10996                self.advance();
10997                Some(crate::ast::ColumnPosition::First)
10998            }
10999            Token::Ident(s) if s.eq_ignore_ascii_case("after") => {
11000                self.advance();
11001                let name = self.expect_ident_like().ok()?;
11002                Some(crate::ast::ColumnPosition::After(name))
11003            }
11004            _ => None,
11005        }
11006    }
11007
11008    fn parse_alter_table_subaction(
11009        &mut self,
11010    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
11011        match self.peek() {
11012            // v7.39.9 — MySQL's own ALTER TABLE vocabulary. Each one is
11013            // a statement a real migration emits and SPG answered 1064
11014            // for; measured against MySQL 9.7.2, one at a time, beside
11015            // the published image.
11016            Token::Ident(s)
11017                if s.eq_ignore_ascii_case("modify") || s.eq_ignore_ascii_case("change") =>
11018            {
11019                let changing = s.eq_ignore_ascii_case("change");
11020                self.advance();
11021                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("column")) {
11022                    self.advance();
11023                }
11024                // `parse_column_def_with_fk` reads the NAME itself, so
11025                // `MODIFY` hands it the column and `CHANGE` eats the old
11026                // name first and lets it read the new one.
11027                let old_name = if changing {
11028                    Some(self.expect_ident_like()?)
11029                } else {
11030                    None
11031                };
11032                let (definition, _fk) = self.parse_column_def_with_fk()?;
11033                let column = old_name.clone().unwrap_or_else(|| definition.name.clone());
11034                let rename_to = if changing {
11035                    Some(definition.name.clone())
11036                } else {
11037                    None
11038                };
11039                let position = self.parse_column_position();
11040                Ok(alloc::vec![crate::ast::AlterTableTarget::ModifyColumn {
11041                    column,
11042                    rename_to,
11043                    definition,
11044                    position,
11045                }])
11046            }
11047            Token::Ident(s) if s.eq_ignore_ascii_case("auto_increment") => {
11048                self.advance();
11049                if matches!(self.peek(), Token::Eq) {
11050                    self.advance();
11051                }
11052                let n = self.expect_u64_literal()?;
11053                Ok(alloc::vec![
11054                    crate::ast::AlterTableTarget::SetTableAutoIncrement(
11055                        i64::try_from(n).unwrap_or(i64::MAX)
11056                    )
11057                ])
11058            }
11059            Token::Ident(s) if s.eq_ignore_ascii_case("engine") => {
11060                self.advance();
11061                if matches!(self.peek(), Token::Eq) {
11062                    self.advance();
11063                }
11064                // v7.39.10 — as WRITTEN, the way `CREATE TABLE`'s ENGINE
11065                // clause has kept it since v7.39.3. The lexer folds a
11066                // bare identifier, and MySQL names the engine back
11067                // exactly: measured, `ALTER TABLE f1 ENGINE=NoSuchEng`
11068                // answers `Unknown storage engine 'NoSuchEng'` there and
11069                // answered `'nosucheng'` here — the one thing that
11070                // message is for is telling the operator which word in
11071                // their migration was wrong.
11072                let at = self.pos;
11073                let name = self.expect_ident_like()?;
11074                let written = self
11075                    .source_span(at, at)
11076                    .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
11077                    .filter(|raw| raw.eq_ignore_ascii_case(&name))
11078                    .map(alloc::string::String::from);
11079                Ok(alloc::vec![crate::ast::AlterTableTarget::SetEngine(
11080                    written.unwrap_or(name)
11081                )])
11082            }
11083            Token::Ident(s) if s.eq_ignore_ascii_case("convert") => {
11084                self.advance();
11085                // CONVERT TO CHARACTER SET <cs> [COLLATE <c>]
11086                if matches!(self.peek(), Token::To) {
11087                    self.advance();
11088                }
11089                let kw = self.expect_ident_like()?;
11090                if !kw.eq_ignore_ascii_case("character") {
11091                    return Err(self.err("expected CHARACTER after CONVERT TO".into()));
11092                }
11093                let set_kw = self.expect_ident_like()?;
11094                if !set_kw.eq_ignore_ascii_case("set") {
11095                    return Err(self.err("expected SET after CHARACTER".into()));
11096                }
11097                let charset = self.expect_ident_like()?;
11098                let collate =
11099                    if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("collate")) {
11100                        self.advance();
11101                        Some(self.expect_ident_like()?)
11102                    } else {
11103                        None
11104                    };
11105                Ok(alloc::vec![
11106                    crate::ast::AlterTableTarget::ConvertToCharacterSet { charset, collate }
11107                ])
11108            }
11109            Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11110                self.advance();
11111                // v7.37.18 (18.7-18.15) — SET ( option = value, … )
11112                // storage parameters: paren-prefixed; consume.
11113                if matches!(self.peek(), Token::LParen) {
11114                    self.consume_until_statement_boundary();
11115                    return Ok(Vec::new());
11116                }
11117                let setting = self.expect_ident_like()?;
11118                if setting.eq_ignore_ascii_case("hot_tier_bytes") {
11119                    if !matches!(self.peek(), Token::Eq) {
11120                        return Err(self.err(alloc::format!(
11121                            "expected '=' after hot_tier_bytes, got {:?}",
11122                            self.peek()
11123                        )));
11124                    }
11125                    self.advance();
11126                    let n = self.expect_u64_literal()?;
11127                    return Ok(alloc::vec![crate::ast::AlterTableTarget::SetHotTierBytes(n)]);
11128                }
11129                // v7.37.18 (18.7 / 18.8 / 18.11 / 18.13 / 18.14) —
11130                // accept-and-no-op for ALTER TABLE SET <subject>
11131                // forms that pg_dump emits but SPG either treats
11132                // as N/A (single-tenant, single-owner, no shared
11133                // tablespaces) or accepts the dump-side declaration
11134                // without runtime effect:
11135                //   SET SCHEMA <name>            (18.11)
11136                //   SET TABLESPACE <name>        (18.8)
11137                //   SET LOGGED / UNLOGGED        (18.7 alt-form)
11138                //   SET WITHOUT CLUSTER          (18.13)
11139                //   SET WITHOUT OIDS             (PG legacy)
11140                //   SET (option = value, …)      (storage parameters)
11141                //   SET REPLICA IDENTITY {…}     (18.14)
11142                if setting.eq_ignore_ascii_case("schema")
11143                    || setting.eq_ignore_ascii_case("tablespace")
11144                    || setting.eq_ignore_ascii_case("logged")
11145                    || setting.eq_ignore_ascii_case("unlogged")
11146                    || setting.eq_ignore_ascii_case("without")
11147                {
11148                    self.consume_until_statement_boundary();
11149                    return Ok(Vec::new());
11150                }
11151                if setting.eq_ignore_ascii_case("replica") {
11152                    // SET REPLICA IDENTITY {DEFAULT|FULL|NOTHING|USING INDEX <name>}
11153                    self.consume_until_statement_boundary();
11154                    return Ok(Vec::new());
11155                }
11156                // SET (option=value, …) — storage parameters.
11157                if matches!(self.peek(), Token::LParen) {
11158                    self.consume_until_statement_boundary();
11159                    return Ok(Vec::new());
11160                }
11161                Err(self.err(alloc::format!(
11162                    "ALTER TABLE SET: unknown setting {setting:?}; supported: \
11163                     hot_tier_bytes / SCHEMA / TABLESPACE / LOGGED / UNLOGGED / \
11164                     WITHOUT CLUSTER / WITHOUT OIDS / REPLICA IDENTITY / (storage_params)"
11165                )))
11166            }
11167            // v7.39 (round 647) — `ALTER TABLE c INHERIT p`. Carried now,
11168            // not ignored: round 645 gave SPG the inheritance the
11169            // v7.37.18 no-op said it did not have.
11170            Token::Ident(s) if s.eq_ignore_ascii_case("inherit") => {
11171                self.advance();
11172                let parent = self.expect_ident_like()?;
11173                self.consume_until_statement_boundary();
11174                Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11175                    parent,
11176                    detach: false
11177                }])
11178            }
11179            // `NO INHERIT <parent>`. Guarded to NOT match `NO FORCE ROW
11180            // LEVEL SECURITY`, which has its own RLS arm below — without
11181            // the guard this swallowed NO FORCE as a no-op.
11182            Token::Ident(s)
11183                if s.eq_ignore_ascii_case("no")
11184                    && !matches!(
11185                        self.tokens.get(self.pos + 1),
11186                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11187                    ) =>
11188            {
11189                self.advance();
11190                if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
11191                    if k.eq_ignore_ascii_case("inherit"))
11192                {
11193                    self.advance();
11194                    let parent = self.expect_ident_like()?;
11195                    self.consume_until_statement_boundary();
11196                    return Ok(alloc::vec![crate::ast::AlterTableTarget::Inherit {
11197                        parent,
11198                        detach: true
11199                    }]);
11200                }
11201                self.consume_until_statement_boundary();
11202                Ok(Vec::new())
11203            }
11204            // v7.37.18 (18.10) — ALTER TABLE OWNER TO <user>. SPG is
11205            // single-owner, so there is still nothing to record.
11206            //
11207            // v7.39 (round 652) — but the name now reaches the engine,
11208            // which refuses a role that does not exist as PG does. The
11209            // no-op was swallowing the whole statement, so a dump naming
11210            // a role this server never heard of restored clean and left
11211            // the table owned by whoever ran the restore.
11212            Token::Ident(s) if s.eq_ignore_ascii_case("owner") => {
11213                self.advance();
11214                if matches!(self.peek(), Token::To) {
11215                    self.advance();
11216                }
11217                let role = self.expect_ident_like()?;
11218                Ok(alloc::vec![crate::ast::AlterTableTarget::OwnerTo {
11219                    role
11220                }])
11221            }
11222            // v7.37.18 (18.13) — ALTER TABLE CLUSTER ON <index>.
11223            // PG sets a hint; SPG doesn't have clustered storage, so the
11224            // hint itself stays a no-op.
11225            //
11226            // v7.39 (round 652) — the index name is checked now. PG
11227            // errors on one that does not exist, and swallowing that let
11228            // a typo'd CLUSTER ON pass silently.
11229            Token::Ident(s) if s.eq_ignore_ascii_case("cluster") => {
11230                self.advance();
11231                // `ON` is a reserved token, not an ident.
11232                if !matches!(self.peek(), Token::On) {
11233                    return Err(self.err(alloc::format!(
11234                        "expected ON after CLUSTER, got {:?}",
11235                        self.peek()
11236                    )));
11237                }
11238                self.advance();
11239                let index = self.expect_ident_like()?;
11240                Ok(alloc::vec![crate::ast::AlterTableTarget::ClusterOn {
11241                    index: Some(index)
11242                }])
11243            }
11244            // v7.39 (read01 round 49) — ALTER TABLE REPLICA IDENTITY
11245            // { DEFAULT | FULL | NOTHING | USING INDEX <name> }. PG records
11246            // what a logical decoder puts in the old-tuple image; SPG's
11247            // replication is SQL-text, so there is nothing to record.
11248            // Accept-and-no-op (it used to be a parse error).
11249            Token::Ident(s) if s.eq_ignore_ascii_case("replica") => {
11250                self.advance();
11251                // v7.39 (round 710) — `REPLICA IDENTITY USING INDEX <i>`
11252                // validates the index; DEFAULT / FULL / NOTHING stay no-op.
11253                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("identity"))
11254                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(k)) if k.eq_ignore_ascii_case("using"))
11255                {
11256                    self.advance(); // IDENTITY
11257                    self.advance(); // USING
11258                    if matches!(self.peek(), Token::Index)
11259                        || matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("index"))
11260                    {
11261                        self.advance();
11262                    }
11263                    let index = self.expect_ident_like()?;
11264                    self.consume_until_statement_boundary();
11265                    return Ok(alloc::vec![
11266                        crate::ast::AlterTableTarget::ReplicaIdentityUsingIndex { index }
11267                    ]);
11268                }
11269                self.consume_until_statement_boundary();
11270                Ok(Vec::new())
11271            }
11272            // v7.37.18 (18.15) — ALTER TABLE VALIDATE CONSTRAINT <name>.
11273            //
11274            // v7.39 (round 652) — it used to consume the statement and
11275            // return nothing, on the stated theory that SPG validated at
11276            // ADD CONSTRAINT time so there was never anything left to
11277            // validate. Measured against PG18, ADD CONSTRAINT did not
11278            // scan the existing rows at all — the comment described a
11279            // property SPG did not have, which is why nobody looked. Both
11280            // halves are real now: ADD scans unless told NOT VALID, and
11281            // this scans what NOT VALID skipped.
11282            Token::Ident(s) if s.eq_ignore_ascii_case("validate") => {
11283                self.advance();
11284                self.expect_keyword_ident("constraint")?;
11285                let name = self.expect_ident_like()?;
11286                Ok(alloc::vec![
11287                    crate::ast::AlterTableTarget::ValidateConstraint { name }
11288                ])
11289            }
11290            // v7.37.18 (18.18) — RESET ( option [, …] ). Inverse of
11291            // SET (option = value, …). PG uses it to clear per-table
11292            // storage params like fillfactor or autovacuum_*. SPG
11293            // engine-manages those parameters; accept-and-no-op.
11294            Token::Ident(s) if s.eq_ignore_ascii_case("reset") => {
11295                self.advance();
11296                self.consume_until_statement_boundary();
11297                Ok(Vec::new())
11298            }
11299            // v7.37.18 (18.18) — OF <type_name> / NOT OF. Composite-
11300            // type-of binding (PG 9.0+). SPG composite types
11301            // (v7.37.5 ζ-B sub-commit) follow CREATE TYPE; ALTER
11302            // TABLE OF is rare and inverse of CREATE TABLE OF.
11303            // Accept-and-no-op until a customer dump round-trips it.
11304            Token::Ident(s) if s.eq_ignore_ascii_case("of") => {
11305                self.advance();
11306                // v7.39 (round 710) — the type name is validated now.
11307                let type_name = self.expect_ident_like()?;
11308                self.consume_until_statement_boundary();
11309                Ok(alloc::vec![crate::ast::AlterTableTarget::OfType {
11310                    type_name
11311                }])
11312            }
11313            // v7.37.18 (18.18) — `NOT OF` lexes NOT as Token::Not
11314            // (reserved keyword) rather than Token::Ident("not"),
11315            // so it needs its own arm. Accept-and-no-op same as OF.
11316            Token::Not => {
11317                self.advance();
11318                self.consume_until_statement_boundary();
11319                Ok(Vec::new())
11320            }
11321            // v7.39 (RLS) — FORCE ROW LEVEL SECURITY (sets relforcerowsecurity).
11322            Token::Ident(s) if s.eq_ignore_ascii_case("force") => {
11323                self.advance();
11324                self.expect_row_level_security()?;
11325                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11326                    enabled: None,
11327                    force: Some(true),
11328                }])
11329            }
11330            // v7.39 (RLS) — NO FORCE ROW LEVEL SECURITY.
11331            Token::Ident(s)
11332                if s.eq_ignore_ascii_case("no")
11333                    && matches!(
11334                        self.tokens.get(self.pos + 1),
11335                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("force")
11336                    ) =>
11337            {
11338                self.advance(); // NO
11339                self.advance(); // FORCE
11340                self.expect_row_level_security()?;
11341                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11342                    enabled: None,
11343                    force: Some(false),
11344                }])
11345            }
11346            // v7.39 (RLS) — ENABLE/DISABLE ROW LEVEL SECURITY
11347            // (sets relrowsecurity). The guard requires the next token to be
11348            // `ROW` so the ENABLE/DISABLE TRIGGER arm still matches its case.
11349            Token::Ident(s)
11350                if (s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable"))
11351                    && matches!(
11352                        self.tokens.get(self.pos + 1),
11353                        Some(Token::Ident(t)) if t.eq_ignore_ascii_case("row")
11354                    ) =>
11355            {
11356                let enabled = s.eq_ignore_ascii_case("enable");
11357                self.advance(); // ENABLE/DISABLE
11358                self.expect_row_level_security()?;
11359                Ok(alloc::vec![crate::ast::AlterTableTarget::SetRowSecurity {
11360                    enabled: Some(enabled),
11361                    force: None,
11362                }])
11363            }
11364            Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11365                self.advance();
11366                // v7.39 (round 431) — MySQL's `ALTER TABLE t ADD [UNIQUE]
11367                // {INDEX|KEY} [name] (cols)`, which every ORM migration
11368                // emits. The same grammar CREATE TABLE already accepts
11369                // inline (`KEY idx (a)`, prefix lengths and all), so it goes
11370                // through the SAME parser — an ALTER-only copy would be a
11371                // second place for the two to drift.
11372                if self.peek_mysql_inline_key_start() {
11373                    return Ok(match self.parse_mysql_inline_key()? {
11374                        Some(c) => {
11375                            alloc::vec![crate::ast::AlterTableTarget::AddTableConstraint(c)]
11376                        }
11377                        // FULLTEXT / SPATIAL parse and are accepted as a
11378                        // no-op here exactly as they are inline.
11379                        None => Vec::new(),
11380                    });
11381                }
11382                // v7.14.0 — ADD CONSTRAINT <name> { FOREIGN KEY |
11383                // PRIMARY KEY | UNIQUE | CHECK }. pg_dump emits
11384                // PRIMARY KEY this way; mysqldump emits both.
11385                // Peek-only dispatch (no advance) — `advance()`
11386                // destructively replaces consumed tokens with Eof,
11387                // so saved-pos restore would land on Eofs.
11388                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
11389                {
11390                    // The next-but-one ident is the constraint
11391                    // name; the one after THAT is the kind.
11392                    let kind_pos = self.pos + 2;
11393                    let kind = self.tokens.get(kind_pos).cloned();
11394                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("foreign"))
11395                    {
11396                        let fk = self.parse_table_level_fk()?;
11397                        return Ok(alloc::vec![
11398                            crate::ast::AlterTableTarget::AddForeignKey(fk)
11399                        ]);
11400                    }
11401                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary"))
11402                    {
11403                        self.advance(); // CONSTRAINT
11404                        // v7.39 (read01 round 48) — keep the name; the engine
11405                        // stores it now instead of dropping it on the floor.
11406                        let con_name = self.expect_ident_like()?;
11407                        self.advance(); // PRIMARY
11408                        self.expect_keyword_ident("key")?;
11409                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11410                        // v7.39 (round 711) — the ALTER form carries the
11411                        // timing too (pg_dump writes it here).
11412                        let (deferrable, initially_deferred) =
11413                            self.consume_deferrable_clauses_timed()?;
11414                        return Ok(alloc::vec![
11415                            crate::ast::AlterTableTarget::AddTableConstraint(
11416                                crate::ast::TableConstraint::PrimaryKey {
11417                                    name: Some(con_name),
11418                                    columns: cols,
11419                                    deferrable,
11420                                    initially_deferred,
11421                                }
11422                            )
11423                        ]);
11424                    }
11425                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique"))
11426                    {
11427                        self.advance(); // CONSTRAINT
11428                        // v7.39 (read01 round 48) — keep the name.
11429                        let con_name = self.expect_ident_like()?;
11430                        // v7.22 (mailrs round-13 gap 6) — delegate so
11431                        // the optional `NULLS [NOT] DISTINCT` modifier
11432                        // parses here too (pg_dump emits the ALTER
11433                        // form; semantics enforced by the engine
11434                        // since v7.13).
11435                        let mut uc = self.parse_table_level_unique()?;
11436                        if let crate::ast::TableConstraint::Unique { name, .. } = &mut uc {
11437                            *name = Some(con_name);
11438                        }
11439                        return Ok(alloc::vec![
11440                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11441                        ]);
11442                    }
11443                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check"))
11444                    {
11445                        self.advance(); // CONSTRAINT
11446                        // v7.39 (read01 round 48) — keep the name.
11447                        let con_name = self.expect_ident_like()?;
11448                        self.advance(); // CHECK
11449                        if !matches!(self.peek(), Token::LParen) {
11450                            return Err(self.err(alloc::format!(
11451                                "expected '(' after CHECK, got {:?}", self.peek()
11452                            )));
11453                        }
11454                        self.advance();
11455                        let expr = self.parse_expr(0)?;
11456                        if matches!(self.peek(), Token::RParen) {
11457                            self.advance();
11458                        }
11459                        let not_valid = self.parse_not_valid_suffix();
11460                        return Ok(alloc::vec![
11461                            crate::ast::AlterTableTarget::AddTableConstraint(
11462                                crate::ast::TableConstraint::Check {
11463                                    name: Some(con_name),
11464                                    expr,
11465                                    not_valid,
11466                                }
11467                            )
11468                        ]);
11469                    }
11470                    // v7.39 (round 211) — ADD CONSTRAINT <name> EXCLUDE
11471                    // [USING <am>] (<col> WITH <op>[, …]). pg_dump emits
11472                    // exclusion constraints via this ALTER form.
11473                    if matches!(&kind, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude"))
11474                    {
11475                        self.advance(); // CONSTRAINT
11476                        let con_name = self.expect_ident_like()?;
11477                        let mut ex = self.parse_table_level_exclude()?;
11478                        if let crate::ast::TableConstraint::Exclude { name, .. } = &mut ex {
11479                            *name = Some(con_name);
11480                        }
11481                        return Ok(alloc::vec![
11482                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11483                        ]);
11484                    }
11485                    // Unknown kind — fall through to FK path which
11486                    // produces a descriptive parse error.
11487                }
11488                let is_fk = matches!(
11489                    self.peek(),
11490                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
11491                        || s.eq_ignore_ascii_case("foreign")
11492                );
11493                if is_fk {
11494                    let fk = self.parse_table_level_fk()?;
11495                    return Ok(alloc::vec![crate::ast::AlterTableTarget::AddForeignKey(fk)]);
11496                }
11497                // v7.14.0 — bare ADD PRIMARY KEY / UNIQUE / CHECK
11498                // (no CONSTRAINT prefix) — same dispatch.
11499                match self.peek().clone() {
11500                    Token::Ident(s) if s.eq_ignore_ascii_case("primary") => {
11501                        self.advance();
11502                        self.expect_keyword_ident("key")?;
11503                        let cols = self.parse_paren_ident_list("PRIMARY KEY")?;
11504                        let (deferrable, initially_deferred) =
11505                            self.consume_deferrable_clauses_timed()?;
11506                        return Ok(alloc::vec![
11507                            crate::ast::AlterTableTarget::AddTableConstraint(
11508                                crate::ast::TableConstraint::PrimaryKey {
11509                                    name: None,
11510                                    columns: cols,
11511                                    deferrable,
11512                                    initially_deferred,
11513                                }
11514                            )
11515                        ]);
11516                    }
11517                    Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
11518                        // v7.22 — delegate (NULLS [NOT] DISTINCT).
11519                        let uc = self.parse_table_level_unique()?;
11520                        return Ok(alloc::vec![
11521                            crate::ast::AlterTableTarget::AddTableConstraint(uc)
11522                        ]);
11523                    }
11524                    // v7.39 (round 652) — bare ADD CHECK (no CONSTRAINT
11525                    // prefix). The other three bare forms were here and
11526                    // this one was not, so it fell through to the column
11527                    // path and came back as "unexpected reserved keyword
11528                    // 'check' at start of column definition".
11529                    _ if self.peek_table_level_check_start() => {
11530                        let chk = self.parse_table_level_check()?;
11531                        let not_valid = self.parse_not_valid_suffix();
11532                        let crate::ast::TableConstraint::Check { expr, .. } = chk else {
11533                            unreachable!("parse_table_level_check returns Check")
11534                        };
11535                        return Ok(alloc::vec![
11536                            crate::ast::AlterTableTarget::AddTableConstraint(
11537                                crate::ast::TableConstraint::Check {
11538                                    name: None,
11539                                    expr,
11540                                    not_valid,
11541                                }
11542                            )
11543                        ]);
11544                    }
11545                    // v7.39 (round 211) — bare ADD EXCLUDE (no CONSTRAINT prefix).
11546                    Token::Ident(s) if s.eq_ignore_ascii_case("exclude") => {
11547                        let ex = self.parse_table_level_exclude()?;
11548                        return Ok(alloc::vec![
11549                            crate::ast::AlterTableTarget::AddTableConstraint(ex)
11550                        ]);
11551                    }
11552                    _ => {}
11553                }
11554                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11555                    self.advance();
11556                }
11557                let mut if_not_exists = false;
11558                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11559                    self.advance();
11560                    if !matches!(self.peek(), Token::Not) {
11561                        return Err(self.err(alloc::format!(
11562                            "expected NOT after IF in ALTER TABLE ADD COLUMN, got {:?}",
11563                            self.peek()
11564                        )));
11565                    }
11566                    self.advance();
11567                    if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
11568                        return Err(self.err(alloc::format!(
11569                            "expected EXISTS after IF NOT in ALTER TABLE ADD COLUMN, got {:?}",
11570                            self.peek()
11571                        )));
11572                    }
11573                    self.advance();
11574                    if_not_exists = true;
11575                }
11576                // v7.13.2 — mailrs round-6 S3: `ADD COLUMN col TYPE
11577                // REFERENCES other(col) [ON DELETE …]`. parse_column_def
11578                // returns ColumnDef + an optional inline FK.
11579                let (column, col_level_fk) = self.parse_column_def_with_fk()?;
11580                let col_name = column.name.clone();
11581                // v7.39.9 — MySQL says where the column goes.
11582                let position = self.parse_column_position();
11583                let mut out = alloc::vec![crate::ast::AlterTableTarget::AddColumn {
11584                    column,
11585                    if_not_exists,
11586                    position,
11587                }];
11588                if let Some(mut fk) = col_level_fk {
11589                    if fk.columns.is_empty() {
11590                        fk.columns.push(col_name);
11591                    }
11592                    out.push(crate::ast::AlterTableTarget::AddForeignKey(fk));
11593                }
11594                Ok(out)
11595            }
11596            Token::Drop => {
11597                self.advance();
11598                // v7.13.3 — dispatch on the next token. mailrs round-7
11599                // S8 closed DROP COLUMN; round-6 S7 closed
11600                // DROP CONSTRAINT. Both share IF EXISTS / CASCADE /
11601                // RESTRICT modifiers.
11602                //   DROP CONSTRAINT [IF EXISTS] <name> [CASCADE|RESTRICT]
11603                //   DROP [COLUMN] [IF EXISTS] <col> [CASCADE|RESTRICT]
11604                let subject = match self.peek() {
11605                    Token::Ident(s) if s.eq_ignore_ascii_case("constraint") => {
11606                        self.advance();
11607                        "constraint"
11608                    }
11609                    Token::Ident(s) if s.eq_ignore_ascii_case("column") => {
11610                        self.advance();
11611                        "column"
11612                    }
11613                    // v7.39 (round 431) — MySQL `DROP {INDEX|KEY} name`.
11614                    // `INDEX` lexes as the reserved Token::Index, so it is
11615                    // unambiguous. `KEY` is a plain ident, and PG allows a
11616                    // column literally named "key", so only read it as the
11617                    // keyword when a name follows it.
11618                    Token::Index => {
11619                        self.advance();
11620                        "index"
11621                    }
11622                    Token::Ident(s)
11623                        if s.eq_ignore_ascii_case("key")
11624                            && matches!(
11625                                self.tokens.get(self.pos + 1),
11626                                Some(Token::Ident(_) | Token::QuotedIdent(_))
11627                            ) =>
11628                    {
11629                        self.advance();
11630                        "index"
11631                    }
11632                    // PG-canonical bare `DROP <col>` without COLUMN
11633                    // keyword is also valid; treat any other ident
11634                    // as the column name.
11635                    Token::Ident(_) | Token::QuotedIdent(_) => "column",
11636                    other => {
11637                        return Err(self.err(alloc::format!(
11638                            "expected COLUMN / CONSTRAINT after DROP in ALTER TABLE, got {other:?}"
11639                        )));
11640                    }
11641                };
11642                let mut if_exists = false;
11643                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
11644                    let n1 = self.tokens.get(self.pos + 1);
11645                    if matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")) {
11646                        self.advance();
11647                        self.advance();
11648                        if_exists = true;
11649                    }
11650                }
11651                let name = self.expect_ident_like()?;
11652                let mut cascade = false;
11653                if matches!(
11654                    self.peek(),
11655                    Token::Ident(s) if s.eq_ignore_ascii_case("cascade")
11656                        || s.eq_ignore_ascii_case("restrict")
11657                ) {
11658                    if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cascade"))
11659                    {
11660                        cascade = true;
11661                    }
11662                    self.advance();
11663                }
11664                if subject == "index" {
11665                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropIndex {
11666                        name,
11667                        if_exists,
11668                    }])
11669                } else if subject == "constraint" {
11670                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropForeignKey {
11671                        name,
11672                        if_exists,
11673                    }])
11674                } else {
11675                    Ok(alloc::vec![crate::ast::AlterTableTarget::DropColumn {
11676                        column: name,
11677                        if_exists,
11678                        cascade,
11679                    }])
11680                }
11681            }
11682            Token::Ident(s) if s.eq_ignore_ascii_case("alter") => {
11683                self.advance();
11684                // v7.37.18 (18.16) — `ALTER TABLE … ALTER CONSTRAINT
11685                // <name> {DEFERRABLE|NOT DEFERRABLE} [INITIALLY
11686                // {IMMEDIATE|DEFERRED}]`. SPG enforces constraints
11687                // immediately; accept-and-no-op.
11688                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11689                    self.advance();
11690                    self.consume_until_statement_boundary();
11691                    return Ok(Vec::new());
11692                }
11693                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11694                    self.advance();
11695                }
11696                let col_name = self.expect_ident_like()?;
11697                match self.peek() {
11698                    Token::Ident(s) if s.eq_ignore_ascii_case("type") => {
11699                        self.advance();
11700                    }
11701                    // v7.14.0 — pg_dump emits BIGSERIAL via
11702                    // `ALTER TABLE … ALTER COLUMN id SET DEFAULT
11703                    // nextval('seq')` (the sequence is created
11704                    // separately). SPG's BIGSERIAL already uses
11705                    // AUTO_INCREMENT; accept SET DEFAULT / DROP
11706                    // DEFAULT / SET NOT NULL / DROP NOT NULL as
11707                    // engine no-ops by consuming the tail.
11708                    Token::Ident(s) if s.eq_ignore_ascii_case("set") => {
11709                        // v7.22 (round-13 T2) — `SET DEFAULT
11710                        // nextval('…')` is how pg_dump spells a
11711                        // SERIAL column (plain integer in CREATE
11712                        // TABLE + this ALTER). It used to be
11713                        // swallowed as a no-op, which silently
11714                        // STRIPPED auto-increment from imported
11715                        // schemas — the first post-import INSERT
11716                        // without an explicit id then violated NOT
11717                        // NULL. Lower it to the auto-increment
11718                        // marker instead.
11719                        let is_default_nextval =
11720                            matches!(self.tokens.get(self.pos + 1), Some(Token::Default))
11721                                && matches!(
11722                                    self.tokens.get(self.pos + 2),
11723                                    Some(Token::Ident(f)) if f.eq_ignore_ascii_case("nextval")
11724                                );
11725                        if is_default_nextval {
11726                            let seq_name = self.scan_sequence_name_until_boundary();
11727                            return Ok(alloc::vec![
11728                                crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11729                                    column: col_name,
11730                                    seq_name,
11731                                }
11732                            ]);
11733                        }
11734                        // v7.37.18 (18.1 + 18.2) — proper lowering.
11735                        self.advance(); // consume "set"
11736                        match self.peek().clone() {
11737                            Token::Default => {
11738                                self.advance();
11739                                let default_expr = self.parse_expr(0)?;
11740                                return Ok(alloc::vec![
11741                                    crate::ast::AlterTableTarget::AlterColumnSetDefault {
11742                                        column: col_name,
11743                                        default_expr,
11744                                    }
11745                                ]);
11746                            }
11747                            Token::Not => {
11748                                self.advance();
11749                                if !matches!(self.peek(), Token::Null) {
11750                                    return Err(self.err(alloc::format!(
11751                                        "expected NULL after ALTER COLUMN SET NOT, got {:?}",
11752                                        self.peek()
11753                                    )));
11754                                }
11755                                self.advance();
11756                                return Ok(alloc::vec![
11757                                    crate::ast::AlterTableTarget::AlterColumnSetNotNull {
11758                                        column: col_name,
11759                                    }
11760                                ]);
11761                            }
11762                            // `SET EXPRESSION AS (expr)` (PG 17) — change a
11763                            // stored generated column's expression and
11764                            // recompute existing rows.
11765                            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
11766                                self.advance(); // EXPRESSION
11767                                if matches!(self.peek(), Token::As) {
11768                                    self.advance();
11769                                }
11770                                let expr = self.parse_expr(0)?;
11771                                return Ok(alloc::vec![
11772                                    crate::ast::AlterTableTarget::AlterColumnSetExpression {
11773                                        column: col_name,
11774                                        expr,
11775                                    }
11776                                ]);
11777                            }
11778                            other => {
11779                                // Other SET subjects (STATISTICS,
11780                                // STORAGE, COMPRESSION, …) stay no-ops —
11781                                // storage hints with no SPG semantics.
11782                                let _ = other;
11783                                self.consume_until_statement_boundary();
11784                                return Ok(Vec::new());
11785                            }
11786                        }
11787                    }
11788                    Token::Ident(s) if s.eq_ignore_ascii_case("drop") => {
11789                        self.advance(); // consume "drop"
11790                        return self.parse_alter_column_drop_tail(col_name);
11791                    }
11792                    Token::Drop => {
11793                        self.advance(); // consume Drop token
11794                        return self.parse_alter_column_drop_tail(col_name);
11795                    }
11796                    Token::Ident(s) if s.eq_ignore_ascii_case("add") => {
11797                        // v7.22 (round-13 T2) — `ALTER COLUMN c ADD
11798                        // GENERATED { ALWAYS | BY DEFAULT } AS
11799                        // IDENTITY ( … )`: pg_dump's spelling for
11800                        // identity columns. Same auto-increment
11801                        // lowering as the nextval default; the
11802                        // sequence options inside the parens are
11803                        // no-ops under SPG's max+1 semantics.
11804                        let is_generated = matches!(
11805                            self.tokens.get(self.pos + 1),
11806                            Some(Token::Ident(g)) if g.eq_ignore_ascii_case("generated")
11807                        );
11808                        if !is_generated {
11809                            return Err(self.err(alloc::format!(
11810                                "expected GENERATED after ALTER COLUMN {col_name} ADD, got {:?}",
11811                                self.tokens.get(self.pos + 1)
11812                            )));
11813                        }
11814                        let seq_name = self.scan_sequence_name_until_boundary();
11815                        return Ok(alloc::vec![
11816                            crate::ast::AlterTableTarget::SetColumnAutoIncrement {
11817                                column: col_name,
11818                                seq_name,
11819                            }
11820                        ]);
11821                    }
11822                    // v7.39 (round 220) — `RESTART [WITH n]` on an identity
11823                    // column: floor the next allocated value at n (bare
11824                    // RESTART = restart from the start value, 1).
11825                    Token::Ident(s) if s.eq_ignore_ascii_case("restart") => {
11826                        self.advance();
11827                        let with = if matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
11828                        {
11829                            self.advance();
11830                            let neg = if matches!(self.peek(), Token::Minus) {
11831                                self.advance();
11832                                true
11833                            } else {
11834                                false
11835                            };
11836                            match self.advance() {
11837                                Token::Integer(v) => Some(if neg { -v } else { v }),
11838                                other => {
11839                                    return Err(self.err(alloc::format!(
11840                                        "expected integer after RESTART WITH, got {other:?}"
11841                                    )));
11842                                }
11843                            }
11844                        } else {
11845                            None
11846                        };
11847                        return Ok(alloc::vec![
11848                            crate::ast::AlterTableTarget::AlterColumnRestart {
11849                                column: col_name,
11850                                with,
11851                            }
11852                        ]);
11853                    }
11854                    other => {
11855                        return Err(self.err(alloc::format!(
11856                            "expected TYPE / SET / DROP / ADD after ALTER COLUMN <name>, got {other:?}"
11857                        )));
11858                    }
11859                }
11860                // v7.39 (round 713) — the type parser has consumed a
11861                // trailing `COLLATE <name>` since Phase 2.5, and
11862                // `parse_column_type_name` discarded it: `ALTER COLUMN t
11863                // TYPE text COLLATE "C"` parsed clean and changed
11864                // nothing. Keep the clause; the engine re-collates.
11865                let (new_type, _, _, _, coll, coll_explicit, coll_name, _, _, _, _, _, _, _) =
11866                    self.parse_type_with_implied_flags()?;
11867                let collation = if coll_explicit {
11868                    coll_name.map(|n| (coll, n))
11869                } else {
11870                    None
11871                };
11872                let using = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using"))
11873                {
11874                    self.advance();
11875                    Some(self.parse_expr(0)?)
11876                } else {
11877                    None
11878                };
11879                Ok(alloc::vec![crate::ast::AlterTableTarget::AlterColumnType {
11880                    column: col_name,
11881                    new_type,
11882                    using,
11883                    collation,
11884                }])
11885            }
11886            // v7.15.0 — `ALTER TABLE t RENAME [COLUMN] old TO new`.
11887            // PG also supports `RENAME TO new_table` for table-name
11888            // rename; that surface is deferred (pg_dump never emits
11889            // it). If the first post-RENAME ident is `TO`, the user
11890            // is asking for table rename — error with a clear
11891            // message rather than misparsing `TO` as a column name.
11892            Token::Ident(s) if s.eq_ignore_ascii_case("rename") => {
11893                self.advance();
11894                // v7.16.2 — `ALTER TABLE t RENAME TO new_table`
11895                // table-name rename (mailrs round-10 A.5 — used
11896                // by migrate-042's `RENAME TO email_contacts`).
11897                // `TO` lexes as Token::To.
11898                if matches!(self.peek(), Token::To)
11899                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("to"))
11900                {
11901                    self.advance();
11902                    let new = self.expect_ident_like()?;
11903                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameTable {
11904                        new,
11905                    }]);
11906                }
11907                // v7.39 (read01 round 48) — `RENAME CONSTRAINT old TO new`.
11908                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
11909                    self.advance();
11910                    let old = self.expect_ident_like()?;
11911                    if matches!(self.peek(), Token::To) {
11912                        self.advance();
11913                    } else {
11914                        self.expect_keyword_ident("to")?;
11915                    }
11916                    let new = self.expect_ident_like()?;
11917                    return Ok(alloc::vec![
11918                        crate::ast::AlterTableTarget::RenameConstraint { old, new }
11919                    ]);
11920                }
11921                // v7.39.9 — MySQL's `RENAME {INDEX|KEY} old TO new`.
11922                // PostgreSQL renames an index with its own top-level
11923                // `ALTER INDEX`, so this spelling had nowhere to go and
11924                // answered 1064; MySQL 9.7.2 parses it and answers 1176
11925                // when the key is not there.
11926                if matches!(self.peek(), Token::Index)
11927                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key"))
11928                {
11929                    self.advance();
11930                    let old = self.expect_ident_like()?;
11931                    if matches!(self.peek(), Token::To) {
11932                        self.advance();
11933                    } else {
11934                        self.expect_keyword_ident("to")?;
11935                    }
11936                    let new = self.expect_ident_like()?;
11937                    return Ok(alloc::vec![crate::ast::AlterTableTarget::RenameIndex {
11938                        old,
11939                        new,
11940                    }]);
11941                }
11942                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("column")) {
11943                    self.advance();
11944                }
11945                let old = self.expect_ident_like()?;
11946                // `TO` is a reserved keyword token; accept both
11947                // Token::To and Token::Ident("to") for consistency.
11948                if matches!(self.peek(), Token::To) {
11949                    self.advance();
11950                } else {
11951                    self.expect_keyword_ident("to")?;
11952                }
11953                let new = self.expect_ident_like()?;
11954                Ok(alloc::vec![crate::ast::AlterTableTarget::RenameColumn {
11955                    old,
11956                    new,
11957                }])
11958            }
11959            // v7.16.1 — `ALTER TABLE t { ENABLE | DISABLE } TRIGGER
11960            // { ALL | <name> }`. pg_dump --disable-triggers wraps
11961            // every data block with these. Real disable semantics —
11962            // not no-op — because reload correctness assumes the
11963            // triggers don't fire (rows already carry their
11964            // computed values from prod).
11965            Token::Ident(s)
11966                if s.eq_ignore_ascii_case("enable") || s.eq_ignore_ascii_case("disable") =>
11967            {
11968                let enabled = s.eq_ignore_ascii_case("enable");
11969                self.advance();
11970                // PG also accepts ENABLE/DISABLE { REPLICA | ALWAYS }
11971                // TRIGGER … and ENABLE/DISABLE RULE / ROW LEVEL
11972                // SECURITY. v7.16.1 only matches TRIGGER (mailrs's
11973                // pg_dump output) — anything else falls through to
11974                // the catch-all error below.
11975                // v7.22 (round-13 T3) — mysqldump wraps every data
11976                // section in `/*!40000 ALTER TABLE t DISABLE KEYS */`
11977                // + ENABLE KEYS (a MyISAM index-rebuild hint). SPG
11978                // maintains indexes incrementally — engine no-op.
11979                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("keys")) {
11980                    self.advance();
11981                    return Ok(Vec::new());
11982                }
11983                // v7.37.18 (18.12) — ENABLE/DISABLE ALWAYS TRIGGER
11984                // and ENABLE/DISABLE REPLICA TRIGGER. PG uses these
11985                // to gate triggers on session_replication_role; SPG
11986                // has no replica role, so the prefix is consumed and
11987                // treated identically to the plain ENABLE/DISABLE
11988                // TRIGGER form.
11989                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("always"))
11990                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replica"))
11991                {
11992                    self.advance();
11993                }
11994                if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("trigger")) {
11995                    return Err(self.err(alloc::format!(
11996                        "expected TRIGGER after {}, got {:?}",
11997                        if enabled { "ENABLE" } else { "DISABLE" },
11998                        self.peek()
11999                    )));
12000                }
12001                self.advance();
12002                // `ALL` lexes as Token::All (reserved); also
12003                // accept Token::Ident("all") for symmetry.
12004                // v7.37.18 (18.12) — USER / REPLICA / ALWAYS post-
12005                // TRIGGER selectors. USER (= all user triggers) is
12006                // semantically ALL here; REPLICA / ALWAYS gate on
12007                // session_replication_role which SPG doesn't track.
12008                // All map to TriggerSelector::All.
12009                let which = if matches!(self.peek(), Token::All)
12010                    || matches!(self.peek(), Token::Ident(s)
12011                        if s.eq_ignore_ascii_case("all")
12012                            || s.eq_ignore_ascii_case("user")
12013                            || s.eq_ignore_ascii_case("replica")
12014                            || s.eq_ignore_ascii_case("always"))
12015                {
12016                    self.advance();
12017                    crate::ast::TriggerSelector::All
12018                } else {
12019                    let name = self.expect_ident_like()?;
12020                    crate::ast::TriggerSelector::Named(name)
12021                };
12022                Ok(alloc::vec![crate::ast::AlterTableTarget::SetTriggerEnabled {
12023                    which,
12024                    enabled,
12025                }])
12026            }
12027            // v7.37.16 (16.3) — ATTACH PARTITION child <bounds>
12028            Token::Ident(s) if s.eq_ignore_ascii_case("attach") => {
12029                self.advance();
12030                if !matches!(self.peek(), Token::Partition)
12031                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12032                        if s.eq_ignore_ascii_case("partition"))
12033                {
12034                    return Err(self.err(alloc::format!(
12035                        "expected PARTITION after ATTACH, got {:?}",
12036                        self.peek()
12037                    )));
12038                }
12039                self.advance();
12040                let child = self.expect_ident_like()?;
12041                let bounds = self.parse_partition_bounds_tail()?;
12042                Ok(alloc::vec![
12043                    crate::ast::AlterTableTarget::AttachPartition { child, bounds }
12044                ])
12045            }
12046            // v7.37.16 (16.4 + 16.5) — DETACH PARTITION child [CONCURRENTLY] [FINALIZE]
12047            Token::Ident(s) if s.eq_ignore_ascii_case("detach") => {
12048                self.advance();
12049                if !matches!(self.peek(), Token::Partition)
12050                    && !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12051                        if s.eq_ignore_ascii_case("partition"))
12052                {
12053                    return Err(self.err(alloc::format!(
12054                        "expected PARTITION after DETACH, got {:?}",
12055                        self.peek()
12056                    )));
12057                }
12058                self.advance();
12059                let child = self.expect_ident_like()?;
12060                let mut concurrently = false;
12061                let mut finalize = false;
12062                loop {
12063                    match self.peek().clone() {
12064                        Token::Ident(s) | Token::QuotedIdent(s)
12065                            if s.eq_ignore_ascii_case("concurrently") =>
12066                        {
12067                            self.advance();
12068                            concurrently = true;
12069                        }
12070                        Token::Ident(s) | Token::QuotedIdent(s)
12071                            if s.eq_ignore_ascii_case("finalize") =>
12072                        {
12073                            self.advance();
12074                            finalize = true;
12075                        }
12076                        _ => break,
12077                    }
12078                }
12079                Ok(alloc::vec![crate::ast::AlterTableTarget::DetachPartition {
12080                    child,
12081                    concurrently,
12082                    finalize,
12083                }])
12084            }
12085            other => Err(self.err(alloc::format!(
12086                "expected SET / ADD / DROP / ALTER / RENAME / ENABLE / DISABLE / ATTACH / DETACH in ALTER TABLE, got {other:?}"
12087            ))),
12088        }
12089    }
12090
12091    /// v7.37.16 (16.3) — parse the `FOR VALUES …` / `DEFAULT`
12092    /// tail used by both CREATE TABLE … PARTITION OF and ALTER
12093    /// TABLE … ATTACH PARTITION. Shares the same grammar as
12094    /// `parse_partition_of_tail`'s bounds branch.
12095    /// v7.37.18 (18.1 + 18.2) — parse the tail of `ALTER COLUMN
12096    /// col DROP …`. Accepts `DROP DEFAULT` and `DROP NOT NULL`,
12097    /// lowering each to the respective AlterTableTarget. Any
12098    /// other DROP subject (IDENTITY, EXPRESSION, etc.) stays a
12099    /// no-op via consume_until_statement_boundary.
12100    fn parse_alter_column_drop_tail(
12101        &mut self,
12102        col_name: String,
12103    ) -> Result<Vec<crate::ast::AlterTableTarget>, ParseError> {
12104        match self.peek().clone() {
12105            Token::Default => {
12106                self.advance();
12107                Ok(alloc::vec![
12108                    crate::ast::AlterTableTarget::AlterColumnDropDefault { column: col_name }
12109                ])
12110            }
12111            Token::Not => {
12112                self.advance();
12113                if !matches!(self.peek(), Token::Null) {
12114                    return Err(self.err(alloc::format!(
12115                        "expected NULL after ALTER COLUMN DROP NOT, got {:?}",
12116                        self.peek()
12117                    )));
12118                }
12119                self.advance();
12120                Ok(alloc::vec![
12121                    crate::ast::AlterTableTarget::AlterColumnDropNotNull { column: col_name }
12122                ])
12123            }
12124            // `DROP EXPRESSION [IF EXISTS]` — de-generate a stored
12125            // generated column into a plain column.
12126            Token::Ident(s) if s.eq_ignore_ascii_case("expression") => {
12127                self.advance();
12128                // v7.39 (round 187, U10) — IF EXISTS was consumed but
12129                // dropped, so the engine still errored on a plain
12130                // column; PG's semantics are NOTICE + skip.
12131                let mut if_exists = false;
12132                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12133                    self.advance();
12134                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12135                        self.advance();
12136                        if_exists = true;
12137                    }
12138                }
12139                Ok(alloc::vec![
12140                    crate::ast::AlterTableTarget::AlterColumnDropExpression {
12141                        column: col_name,
12142                        if_exists,
12143                    }
12144                ])
12145            }
12146            // v7.38 (read01, T28) — `DROP IDENTITY [IF EXISTS]` — de-generate an
12147            // identity column into a plain column.
12148            Token::Ident(s) if s.eq_ignore_ascii_case("identity") => {
12149                self.advance();
12150                let mut if_exists = false;
12151                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if")) {
12152                    self.advance();
12153                    if matches!(self.peek(), Token::Ident(e) if e.eq_ignore_ascii_case("exists")) {
12154                        self.advance();
12155                        if_exists = true;
12156                    }
12157                }
12158                Ok(alloc::vec![
12159                    crate::ast::AlterTableTarget::AlterColumnDropIdentity {
12160                        column: col_name,
12161                        if_exists,
12162                    }
12163                ])
12164            }
12165            _ => {
12166                self.consume_until_statement_boundary();
12167                Ok(Vec::new())
12168            }
12169        }
12170    }
12171
12172    /// Parse the optional trailer of `COPY … TO STDOUT`: nothing (text
12173    /// format, no header), the modern `[WITH] ( opt [, opt]* )` list, or
12174    /// the legacy space-separated `[WITH] CSV|TEXT [HEADER] [DELIMITER
12175    /// 'c'] [NULL 'str'] [QUOTE 'c']` spelling.
12176    fn parse_copy_to_options(&mut self) -> Result<crate::ast::CopyOptions, ParseError> {
12177        let mut opts = crate::ast::CopyOptions::default();
12178        if matches!(self.peek(), Token::Eof | Token::Semicolon) {
12179            return Ok(opts);
12180        }
12181        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
12182            self.advance();
12183        }
12184        if matches!(self.peek(), Token::LParen) {
12185            self.advance();
12186            loop {
12187                self.parse_one_copy_option(&mut opts)?;
12188                match self.peek() {
12189                    Token::Comma => {
12190                        self.advance();
12191                    }
12192                    Token::RParen => {
12193                        self.advance();
12194                        break;
12195                    }
12196                    other => {
12197                        return Err(self.err(alloc::format!(
12198                            "expected ',' or ')' in COPY options, got {other:?}"
12199                        )));
12200                    }
12201                }
12202            }
12203        } else {
12204            while !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12205                self.parse_one_copy_option(&mut opts)?;
12206            }
12207        }
12208        if !matches!(self.peek(), Token::Eof | Token::Semicolon) {
12209            return Err(self.err(alloc::format!(
12210                "unexpected token after COPY options: {:?}",
12211                self.peek()
12212            )));
12213        }
12214        Ok(opts)
12215    }
12216
12217    fn parse_one_copy_option(
12218        &mut self,
12219        opts: &mut crate::ast::CopyOptions,
12220    ) -> Result<(), ParseError> {
12221        use crate::ast::CopyFormat;
12222        // The option keyword. NULL lexes as its own token; the rest are
12223        // bare identifiers.
12224        let kw = match self.advance() {
12225            Token::Null => alloc::string::String::from("NULL"),
12226            Token::Ident(s) => s.to_uppercase(),
12227            other => {
12228                return Err(self.err(alloc::format!(
12229                    "expected a COPY option keyword, got {other:?}"
12230                )));
12231            }
12232        };
12233        match kw.as_str() {
12234            "FORMAT" => {
12235                let fmt = self.expect_ident_like()?;
12236                match fmt.to_ascii_uppercase().as_str() {
12237                    "CSV" => opts.format = CopyFormat::Csv,
12238                    "TEXT" => opts.format = CopyFormat::Text,
12239                    other => {
12240                        return Err(self.err(alloc::format!(
12241                            "COPY format \"{}\" not recognized",
12242                            other.to_ascii_lowercase()
12243                        )));
12244                    }
12245                }
12246            }
12247            // Legacy bare format keywords.
12248            "CSV" => opts.format = CopyFormat::Csv,
12249            "TEXT" => opts.format = CopyFormat::Text,
12250            "HEADER" => {
12251                opts.header = match self.peek() {
12252                    Token::True => {
12253                        self.advance();
12254                        true
12255                    }
12256                    Token::False => {
12257                        self.advance();
12258                        false
12259                    }
12260                    Token::Ident(s) if s.eq_ignore_ascii_case("on") => {
12261                        self.advance();
12262                        true
12263                    }
12264                    Token::Ident(s) if s.eq_ignore_ascii_case("off") => {
12265                        self.advance();
12266                        false
12267                    }
12268                    // Bare HEADER (no boolean) means HEADER true.
12269                    _ => true,
12270                };
12271            }
12272            // r1066 (7.38 S5.1) — pgbench 14+ loads with
12273            // `COPY … WITH (FREEZE ON)`. The hint's PG effect is
12274            // vacuum bookkeeping on a freshly created/truncated
12275            // table; SPG's per-statement visibility makes it a
12276            // faithful no-op, and rejecting it aborted `pgbench -i`
12277            // against the drop-in. Accept ON/OFF/bare, change nothing.
12278            "FREEZE" => match self.peek() {
12279                Token::True | Token::False => {
12280                    self.advance();
12281                }
12282                Token::Ident(s)
12283                    if s.eq_ignore_ascii_case("on") || s.eq_ignore_ascii_case("off") =>
12284                {
12285                    self.advance();
12286                }
12287                _ => {}
12288            },
12289            "DELIMITER" | "QUOTE" | "ESCAPE" => {
12290                let s = match self.advance() {
12291                    Token::String(s) => s,
12292                    other => {
12293                        return Err(self.err(alloc::format!(
12294                            "COPY {kw} expects a single-character string, got {other:?}"
12295                        )));
12296                    }
12297                };
12298                // v7.39 (round 247) — PG's wording (0A000), keyword in
12299                // lowercase: "COPY delimiter must be a single one-byte
12300                // character".
12301                let one_byte_err = || {
12302                    self.err(alloc::format!(
12303                        "COPY {} must be a single one-byte character",
12304                        kw.to_ascii_lowercase()
12305                    ))
12306                };
12307                let mut chars = s.chars();
12308                let c = chars.next().ok_or_else(one_byte_err)?;
12309                if chars.next().is_some() || c.len_utf8() != 1 {
12310                    return Err(one_byte_err());
12311                }
12312                match kw.as_str() {
12313                    "DELIMITER" => opts.delimiter = Some(c),
12314                    "QUOTE" => opts.quote = Some(c),
12315                    _ => opts.escape = Some(c),
12316                }
12317            }
12318            // v7.39 (round 247) — `FORCE_QUOTE (col, …)` / `FORCE_QUOTE *`.
12319            "FORCE_QUOTE" => {
12320                if matches!(self.peek(), Token::Star) {
12321                    self.advance();
12322                    opts.force_quote = Some(Vec::new());
12323                } else {
12324                    if !matches!(self.peek(), Token::LParen) {
12325                        return Err(self.err(alloc::format!(
12326                            "expected '(' or '*' after FORCE_QUOTE, got {:?}",
12327                            self.peek()
12328                        )));
12329                    }
12330                    self.advance();
12331                    let mut cols = Vec::new();
12332                    loop {
12333                        cols.push(self.expect_ident_like()?);
12334                        match self.peek() {
12335                            Token::Comma => {
12336                                self.advance();
12337                            }
12338                            Token::RParen => {
12339                                self.advance();
12340                                break;
12341                            }
12342                            other => {
12343                                return Err(self.err(alloc::format!(
12344                                    "expected ',' or ')' in FORCE_QUOTE list, got {other:?}"
12345                                )));
12346                            }
12347                        }
12348                    }
12349                    opts.force_quote = Some(cols);
12350                }
12351            }
12352            "NULL" => {
12353                opts.null_str = Some(match self.advance() {
12354                    Token::String(s) => s,
12355                    other => {
12356                        return Err(self.err(alloc::format!(
12357                            "COPY NULL expects a quoted string, got {other:?}"
12358                        )));
12359                    }
12360                });
12361            }
12362            // v7.39 (round 265) — the two CSV FROM-side column lists. Same
12363            // grammar as FORCE_QUOTE; PG accepts `*` for FORCE_NOT_NULL /
12364            // FORCE_NULL too.
12365            "FORCE_NOT_NULL" | "FORCE_NULL" => {
12366                let cols = self.parse_copy_column_list(&kw)?;
12367                if kw == "FORCE_NOT_NULL" {
12368                    opts.force_not_null = Some(cols);
12369                } else {
12370                    opts.force_null = Some(cols);
12371                }
12372            }
12373            other => {
12374                // PG's wording, lowercased option name.
12375                return Err(self.err(alloc::format!(
12376                    "option \"{}\" not recognized",
12377                    other.to_ascii_lowercase()
12378                )));
12379            }
12380        }
12381        Ok(())
12382    }
12383
12384    /// v7.39 (round 265) — `( col, … )` or `*` after a COPY column-list
12385    /// option (FORCE_QUOTE / FORCE_NOT_NULL / FORCE_NULL). An empty vec
12386    /// is the `*` spelling.
12387    fn parse_copy_column_list(&mut self, kw: &str) -> Result<Vec<String>, ParseError> {
12388        if matches!(self.peek(), Token::Star) {
12389            self.advance();
12390            return Ok(Vec::new());
12391        }
12392        if !matches!(self.peek(), Token::LParen) {
12393            return Err(self.err(alloc::format!(
12394                "expected '(' or '*' after {kw}, got {:?}",
12395                self.peek()
12396            )));
12397        }
12398        self.advance();
12399        let mut cols = Vec::new();
12400        loop {
12401            cols.push(self.expect_ident_like()?);
12402            match self.peek() {
12403                Token::Comma => {
12404                    self.advance();
12405                }
12406                Token::RParen => {
12407                    self.advance();
12408                    break;
12409                }
12410                other => {
12411                    return Err(self.err(alloc::format!(
12412                        "expected ',' or ')' in {kw} list, got {other:?}"
12413                    )));
12414                }
12415            }
12416        }
12417        Ok(cols)
12418    }
12419
12420    fn parse_partition_bounds_tail(
12421        &mut self,
12422    ) -> Result<crate::ast::PartitionOfBoundsAst, ParseError> {
12423        use crate::ast::PartitionOfBoundsAst;
12424        match self.peek() {
12425            Token::Default => {
12426                self.advance();
12427                Ok(PartitionOfBoundsAst::Default)
12428            }
12429            Token::For => {
12430                self.advance();
12431                if !matches!(self.peek(), Token::Values) {
12432                    return Err(
12433                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
12434                    );
12435                }
12436                self.advance();
12437                let want_with = matches!(
12438                    self.peek(),
12439                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
12440                );
12441                if want_with {
12442                    self.advance();
12443                    if !matches!(self.peek(), Token::LParen) {
12444                        return Err(self.err(format!(
12445                            "expected '(' after FOR VALUES WITH, got {:?}",
12446                            self.peek()
12447                        )));
12448                    }
12449                    self.advance();
12450                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
12451                    loop {
12452                        let key = self.expect_ident_like()?;
12453                        let n = match self.peek().clone() {
12454                            Token::Integer(v) if u32::try_from(v).is_ok() => {
12455                                self.advance();
12456                                v as u32
12457                            }
12458                            other => {
12459                                return Err(self.err(format!(
12460                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
12461                                )));
12462                            }
12463                        };
12464                        match key.to_ascii_uppercase().as_str() {
12465                            "MODULUS" => modulus = Some(n),
12466                            "REMAINDER" => remainder = Some(n),
12467                            other => {
12468                                return Err(self.err(format!(
12469                                    "FOR VALUES WITH: unknown key {other:?}; \
12470                                     expected MODULUS or REMAINDER"
12471                                )));
12472                            }
12473                        }
12474                        match self.peek() {
12475                            Token::Comma => {
12476                                self.advance();
12477                            }
12478                            Token::RParen => {
12479                                self.advance();
12480                                break;
12481                            }
12482                            other => {
12483                                return Err(self.err(format!(
12484                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
12485                                )));
12486                            }
12487                        }
12488                    }
12489                    let modulus = modulus
12490                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
12491                    let remainder = remainder.ok_or_else(|| {
12492                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
12493                    })?;
12494                    if modulus == 0 {
12495                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
12496                    }
12497                    if remainder >= modulus {
12498                        return Err(self.err(format!(
12499                            "FOR VALUES WITH: REMAINDER ({remainder}) must be < MODULUS ({modulus})"
12500                        )));
12501                    }
12502                    return Ok(PartitionOfBoundsAst::Hash { modulus, remainder });
12503                }
12504                match self.peek() {
12505                    Token::From => {
12506                        self.advance();
12507                        let lower = Box::new(self.parse_partition_bound_expr()?);
12508                        if !matches!(self.peek(), Token::To) {
12509                            return Err(self.err(format!(
12510                                "expected TO after FROM (...), got {:?}",
12511                                self.peek()
12512                            )));
12513                        }
12514                        self.advance();
12515                        let upper = Box::new(self.parse_partition_bound_expr()?);
12516                        Ok(PartitionOfBoundsAst::Range { lower, upper })
12517                    }
12518                    Token::In => {
12519                        self.advance();
12520                        if !matches!(self.peek(), Token::LParen) {
12521                            return Err(self.err(format!(
12522                                "expected '(' after FOR VALUES IN, got {:?}",
12523                                self.peek()
12524                            )));
12525                        }
12526                        self.advance();
12527                        let mut values = Vec::new();
12528                        loop {
12529                            values.push(self.parse_expr(0)?);
12530                            match self.peek() {
12531                                Token::Comma => {
12532                                    self.advance();
12533                                }
12534                                Token::RParen => {
12535                                    self.advance();
12536                                    break;
12537                                }
12538                                other => {
12539                                    return Err(self.err(format!(
12540                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
12541                                    )));
12542                                }
12543                            }
12544                        }
12545                        if values.is_empty() {
12546                            return Err(
12547                                self.err("FOR VALUES IN requires at least one literal".to_string())
12548                            );
12549                        }
12550                        Ok(PartitionOfBoundsAst::List { values })
12551                    }
12552                    other => Err(self.err(format!(
12553                        "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
12554                    ))),
12555                }
12556            }
12557            other => Err(self.err(format!(
12558                "expected DEFAULT or FOR VALUES after ATTACH PARTITION child, got {other:?}"
12559            ))),
12560        }
12561    }
12562
12563    /// v7.16.2 — peek for `information_schema.<tbl>` /
12564    /// `pg_catalog.<tbl>` triples and, if matched, consume all
12565    /// three tokens + return a synthetic table name the engine's
12566    /// SELECT path recognises as a virtual view. Returns `None`
12567    /// when the head doesn't look like a meta-qualified name.
12568    /// Used by `parse_table_ref` to bypass the
12569    /// `expect_ident_like` schema-strip for these specific PG
12570    /// meta schemas (mailrs round-10 A.3).
12571    fn try_peek_meta_qualified(&mut self) -> Option<(String, String)> {
12572        // Extract the schema name. Must be a plain ident token.
12573        let schema = match self.tokens.get(self.pos) {
12574            Some(Token::Ident(s) | Token::QuotedIdent(s)) => s.clone(),
12575            _ => return None,
12576        };
12577        // Dot.
12578        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12579            return None;
12580        }
12581        // The table-side ident may lex as a reserved keyword
12582        // (e.g. `Token::Tables`). Tolerate the common ones via a
12583        // helper that reads the trailing token's underlying name.
12584        let tbl = match self.tokens.get(self.pos + 2)? {
12585            Token::Ident(t) | Token::QuotedIdent(t) => t.clone(),
12586            Token::Tables => "tables".to_string(),
12587            // Other PG meta table names that may collide with
12588            // reserved keywords land here as needed.
12589            _ => return None,
12590        };
12591        // Strip the `pg_` prefix from `pg_catalog.pg_class`-style
12592        // names so the synthetic name doesn't double-prefix
12593        // (`__spg_pg_class`, not `__spg_pg_pg_class`).
12594        let (prefix, normalised) = if schema.eq_ignore_ascii_case("information_schema") {
12595            ("__spg_info_", tbl.to_ascii_lowercase())
12596        } else if schema.eq_ignore_ascii_case("pg_catalog") {
12597            // v7.39 (round 541) — only the catalogs SPG actually
12598            // synthesises are rewritten, which is what the BARE path
12599            // has always checked. Anything else keeps its own name and
12600            // takes the ordinary route: `pg_stat_activity` and friends
12601            // resolve through meta_view_result, and a name that is no
12602            // catalog at all gets PG's "relation does not exist"
12603            // instead of a message about a view SPG cannot materialise.
12604            let lowered = tbl.to_ascii_lowercase();
12605            if !SYNTHESISED_PG_CATALOGS.contains(&lowered.as_str()) {
12606                self.advance(); // schema
12607                self.advance(); // dot
12608                self.advance(); // tbl
12609                return Some((lowered.clone(), lowered));
12610            }
12611            let bare = lowered
12612                .strip_prefix("pg_")
12613                .map(alloc::string::String::from)
12614                .unwrap_or(lowered);
12615            ("__spg_pg_", bare)
12616        } else if schema.eq_ignore_ascii_case("mysql") {
12617            // v7.17.0 Phase 3.P0-65 — MySQL system schema
12618            // (`mysql.user`, `mysql.db`). Same synthetic-name
12619            // shape as pg_catalog.
12620            ("__spg_mysql_", tbl.to_ascii_lowercase())
12621        } else {
12622            return None;
12623        };
12624        self.advance(); // schema
12625        self.advance(); // dot
12626        self.advance(); // tbl
12627        Some((
12628            alloc::format!("{prefix}{normalised}"),
12629            tbl.to_ascii_lowercase(),
12630        ))
12631    }
12632
12633    /// Unqualified PG meta-table names (`FROM pg_extension`, `FROM
12634    /// pg_class`) resolve the same way: PG puts `pg_catalog` at the
12635    /// implicit front of every search_path, so a bare reference to a
12636    /// known catalog table always means the catalog table. Only the
12637    /// names the engine actually synthesises are recognised — any
12638    /// other `pg_*` ident stays a user table (mailrs embed round-12).
12639    fn try_peek_meta_bare(&mut self) -> Option<(String, String)> {
12640        // v7.38 (read01 P3.21) — every catalog view SPG synthesises
12641        // (`__spg_pg_*`) is bare-resolvable, matching PG's implicit
12642        // `pg_catalog` at the front of every search_path. (pg_stat_activity
12643        // / pg_stat_statements / pg_locks / pg_statio_user_tables route
12644        // through the meta_view_result path instead, and already resolve
12645        // bare — they must NOT be listed here or the __spg_ rewrite would
12646        // mis-target them.)
12647        const PG_META_TABLES: &[&str] = SYNTHESISED_PG_CATALOGS;
12648        let name = match self.tokens.get(self.pos) {
12649            Some(Token::Ident(s)) => s.to_ascii_lowercase(),
12650            _ => return None,
12651        };
12652        // A following dot means this ident is a schema qualifier,
12653        // not a table name — let the qualified path handle it.
12654        if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot)) {
12655            return None;
12656        }
12657        if !PG_META_TABLES.contains(&name.as_str()) {
12658            return None;
12659        }
12660        self.advance();
12661        let bare = name.strip_prefix("pg_").unwrap_or(&name);
12662        Some((alloc::format!("__spg_pg_{bare}"), name.clone()))
12663    }
12664
12665    /// Consume a bare ident if its lowercase matches `kw`, else err.
12666    /// v7.39 (read01 round 57) — is the next token this bare keyword-ident?
12667    /// Peeks only; the caller advances.
12668    fn peek_keyword_ident(&self, kw: &str) -> bool {
12669        matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
12670    }
12671
12672    fn expect_keyword_ident(&mut self, kw: &str) -> Result<(), ParseError> {
12673        match self.advance() {
12674            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw) => Ok(()),
12675            other => Err(ParseError {
12676                message: format!("expected {kw:?}, got {other:?}"),
12677                token_pos: self.consumed_pos(),
12678            }),
12679        }
12680    }
12681
12682    /// Accept either a quoted identifier (`"foo"`) or a quoted string
12683    /// literal (`'foo'`) — same shape used by CREATE USER for the
12684    /// username slot.
12685    fn expect_ident_or_string(&mut self) -> Result<String, ParseError> {
12686        match self.advance() {
12687            Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => Ok(s),
12688            other => Err(ParseError {
12689                message: format!("expected identifier or string, got {other:?}"),
12690                token_pos: self.consumed_pos(),
12691            }),
12692        }
12693    }
12694
12695    fn expect_string_literal(&mut self) -> Result<String, ParseError> {
12696        match self.advance() {
12697            Token::String(s) => Ok(s),
12698            other => Err(ParseError {
12699                message: format!("expected quoted string, got {other:?}"),
12700                token_pos: self.consumed_pos(),
12701            }),
12702        }
12703    }
12704
12705    fn parse_select_stmt(&mut self) -> Result<Statement, ParseError> {
12706        // v7.30.2 (mailrs round-25 ask 2) — derived tables /
12707        // subqueries recurse through here without passing
12708        // parse_expr; share the same nesting budget.
12709        self.enter_nested()?;
12710        let r = self.parse_select_stmt_inner();
12711        self.nest_depth -= 1;
12712        r
12713    }
12714
12715    fn parse_select_stmt_inner(&mut self) -> Result<Statement, ParseError> {
12716        // Caller dispatches on Token::Select; the inner helper handles
12717        // the rest. ORDER BY / LIMIT bind at this top level; UNION peers
12718        // get a fresh bare-select parse and may not have their own ORDER
12719        // BY / LIMIT.
12720        let mut head = self.parse_bare_select()?;
12721        let into = self.pending_select_into.take();
12722        self.parse_setop_chain_into(&mut head)?;
12723        self.parse_select_tail_into(&mut head)?;
12724        // v7.38.19 — `SELECT … INTO t` lowers to the SAME node as
12725        // `CREATE TABLE t AS SELECT …`, which is what a comment in
12726        // `ast.rs` has claimed since v7.38 and what only CTAS actually
12727        // did. The tail (ORDER BY / LIMIT) is parsed first so it belongs
12728        // to the body, as it does in PostgreSQL.
12729        if let Some((name, temporary)) = into {
12730            return Ok(Statement::CreateMaterializedView(
12731                crate::ast::CreateMaterializedViewStatement {
12732                    temporary,
12733                    name,
12734                    if_not_exists: false,
12735                    columns: Vec::new(),
12736                    body: head,
12737                    with_data: true,
12738                    as_plain_table: true,
12739                },
12740            ));
12741        }
12742        Ok(Statement::Select(head))
12743    }
12744
12745    /// v7.37.17 (17.6 siblings) — the three SQL set operations
12746    /// share the peer chain: UNION [ALL], EXCEPT [ALL] (a reserved
12747    /// token), and INTERSECT [ALL] (a bare ident — it was never
12748    /// reserved in SPG's lexer). PG precedence: INTERSECT binds
12749    /// tighter than UNION / EXCEPT — the executor folds the chain
12750    /// left-to-right, which is already correct for LEADING
12751    /// intersects; an INTERSECT pair that FOLLOWS a union/except
12752    /// pair nests into that previous peer, so A UNION B INTERSECT C
12753    /// = A ∪ (B ∩ C). Shared by the top level and parenthesized
12754    /// groups.
12755    fn parse_setop_chain_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12756        // A parenthesized group arrives with its own (already
12757        // regrouped) unions on `head`; only the pairs THIS chain
12758        // appends participate in the precedence regroup below —
12759        // nesting an outer INTERSECT into a group-internal peer
12760        // would dissolve the explicit grouping.
12761        let boundary = head.unions.len();
12762        loop {
12763            let base = match self.peek() {
12764                Token::Union => UnionKind::Distinct,
12765                Token::Except => UnionKind::Except,
12766                Token::Ident(s) if s.eq_ignore_ascii_case("intersect") => UnionKind::Intersect,
12767                _ => break,
12768            };
12769            self.advance();
12770            let kind = if matches!(self.peek(), Token::All) {
12771                self.advance();
12772                match base {
12773                    UnionKind::Distinct => UnionKind::All,
12774                    UnionKind::Except => UnionKind::ExceptAll,
12775                    _ => UnionKind::IntersectAll,
12776                }
12777            } else {
12778                base
12779            };
12780            let peer = self.parse_bare_select()?;
12781            head.unions.push((kind, peer));
12782        }
12783        let mut pairs = core::mem::take(&mut head.unions);
12784        let tail = pairs.split_off(boundary);
12785        let mut regrouped: Vec<(UnionKind, SelectStatement)> = pairs;
12786        for (kind, peer) in tail {
12787            let is_intersect = matches!(kind, UnionKind::Intersect | UnionKind::IntersectAll);
12788            // An intersect nests into the previous element of THIS
12789            // chain only; with no new previous element it stays at
12790            // the outer level (the left fold applies it to the
12791            // whole head, group included).
12792            match (
12793                is_intersect,
12794                regrouped.len() > boundary,
12795                regrouped.last_mut(),
12796            ) {
12797                (true, true, Some((_, prev))) => prev.unions.push((kind, peer)),
12798                _ => regrouped.push((kind, peer)),
12799            }
12800        }
12801        head.unions = regrouped;
12802        Ok(())
12803    }
12804
12805    /// v7.37.17 (17.6 siblings) — the shared SELECT tail: ORDER BY /
12806    /// LIMIT / OFFSET / FETCH FIRST / FOR-lock clauses. Extracted so
12807    /// the top-level bare VALUES statement reuses it verbatim.
12808    /// v6.4.0 — parse an optional `ORDER BY <expr> [ASC|DESC] [NULLS …], …`
12809    /// clause into its key list (empty when no `ORDER BY` follows). Extracted
12810    /// (v7.39 round 135) so the grouping-set path can parse ORDER BY early,
12811    /// where the grouping-set universe is still in scope.
12812    fn parse_order_by_keys(&mut self) -> Result<Vec<OrderBy>, ParseError> {
12813        if !matches!(self.peek(), Token::Order) {
12814            return Ok(Vec::new());
12815        }
12816        self.advance();
12817        if !self.peek_is_by() {
12818            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
12819        }
12820        self.advance();
12821        let mut keys = Vec::new();
12822        loop {
12823            // v7.39 (round 691) — save/restore, the discipline this parser
12824            // already uses around `pending_sample_preds`, so a subquery inside
12825            // a key neither inherits nor leaks the channel.
12826            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
12827            let saved_coll = self.order_key_collation.take();
12828            let parsed = self.parse_expr(0);
12829            self.in_order_by_key = saved_flag;
12830            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
12831            let expr = parsed?;
12832            let desc = if matches!(self.peek(), Token::Desc) {
12833                self.advance();
12834                true
12835            } else if matches!(self.peek(), Token::Asc) {
12836                self.advance();
12837                false
12838            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
12839                // `ORDER BY x USING <op>` — PG's operator-class spelling. SPG has
12840                // one ordering per type, so the btree comparison operators map
12841                // onto it: < / <= are ASC, > / >= are DESC. Any other operator
12842                // would need a custom operator class — honest error.
12843                self.advance();
12844                match self.advance() {
12845                    Token::Lt | Token::LtEq => false,
12846                    Token::Gt | Token::GtEq => true,
12847                    other => {
12848                        return Err(self.err(alloc::format!(
12849                            "ORDER BY USING supports the btree comparison \
12850                             operators (< <= > >=); got {other:?}"
12851                        )));
12852                    }
12853                }
12854            } else {
12855                false
12856            };
12857            // v7.24 (round-16 A) — explicit NULLS FIRST/LAST.
12858            let nulls_first = self.parse_optional_nulls_placement()?;
12859            keys.push(OrderBy {
12860                expr,
12861                desc,
12862                nulls_first,
12863                collation,
12864            });
12865            if matches!(self.peek(), Token::Comma) {
12866                self.advance();
12867            } else {
12868                break;
12869            }
12870        }
12871        Ok(keys)
12872    }
12873
12874    fn parse_select_tail_into(&mut self, head: &mut SelectStatement) -> Result<(), ParseError> {
12875        // v7.39 (round 135) — a grouping-set query may have already parsed +
12876        // rewritten its ORDER BY (to reference synthetic grouping columns); if
12877        // no ORDER BY token is present, keep that pre-set order_by rather than
12878        // clobbering it with an empty list.
12879        let parsed_keys = self.parse_order_by_keys()?;
12880        head.order_by = if parsed_keys.is_empty() {
12881            core::mem::take(&mut head.order_by)
12882        } else {
12883            parsed_keys
12884        };
12885        // v7.39 (round 314, V39) — the row-count clauses come in EITHER
12886        // order. PG's grammar takes a limit clause and an offset clause
12887        // as an unordered pair, so `OFFSET 2 LIMIT 3` means exactly what
12888        // `LIMIT 3 OFFSET 2` does (measured: same rows). This used to
12889        // parse them in a fixed LIMIT-then-OFFSET sequence, so the other
12890        // spelling died on `expected end of input, got Limit`.
12891        //
12892        // Each may appear at most once, and LIMIT and FETCH FIRST are
12893        // two spellings of the same clause — PG rejects `LIMIT 1 LIMIT 2`,
12894        // `OFFSET 1 OFFSET 2` and `LIMIT 2 FETCH FIRST 3 ROWS ONLY` alike.
12895        // A second one is left unconsumed here, which the caller reports
12896        // as trailing input rather than silently taking the last.
12897        let mut saw_limit = false;
12898        let mut saw_offset = false;
12899        loop {
12900            if !saw_limit && matches!(self.peek(), Token::Limit) {
12901                self.advance();
12902                // v7.17.0 Phase 5.1 — `LIMIT NULL` / `LIMIT ALL` are
12903                // PG synonyms for "no limit". Treat both as None
12904                // (no head.limit set) so the engine's existing
12905                // unlimited-result path takes over. Reject was the
12906                // pre-5.1 behaviour and broke pg_dump-flavoured
12907                // tooling that occasionally emits LIMIT NULL.
12908                if self.consume_limit_unbounded_sentinel() {
12909                    head.limit = None;
12910                } else {
12911                    let first = self.parse_limit_expr("LIMIT")?;
12912                    // MySQL `LIMIT offset, count` — the first number is
12913                    // the offset when a comma follows.
12914                    if matches!(self.peek(), Token::Comma) {
12915                        self.advance();
12916                        let count = self.parse_limit_expr("LIMIT")?;
12917                        head.offset = Some(first);
12918                        saw_offset = true;
12919                        head.limit = Some(count);
12920                    } else {
12921                        head.limit = Some(first);
12922                    }
12923                }
12924                saw_limit = true;
12925                continue;
12926            }
12927            if !saw_offset && matches!(self.peek(), Token::Offset) {
12928                self.advance();
12929                // PG also accepts an optional `ROW` / `ROWS` trailer
12930                // after the offset value (`OFFSET 10 ROWS`). The
12931                // FETCH-FIRST branch below relies on the same.
12932                let off = self.parse_limit_expr("OFFSET")?;
12933                self.consume_optional_rows_keyword();
12934                head.offset = Some(off);
12935                saw_offset = true;
12936                continue;
12937            }
12938            // v7.17.0 Phase 5.1 — `FETCH FIRST <int|$N> ROWS ONLY` is
12939            // the SQL-standard alias for LIMIT. PG accepts both
12940            // spellings interchangeably; pg_dump emits FETCH FIRST in
12941            // newer versions. We map it onto `head.limit` so the
12942            // engine path is unified.
12943            if !saw_limit
12944                && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12945                    if s.eq_ignore_ascii_case("fetch"))
12946            {
12947                self.advance(); // FETCH
12948                // `FIRST` or `NEXT` (both legal per SQL standard).
12949                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12950                    if s.eq_ignore_ascii_case("first") || s.eq_ignore_ascii_case("next"))
12951                {
12952                    self.advance();
12953                }
12954                // Count (optional in the bare `FETCH FIRST ROW ONLY` —
12955                // implicit 1 — but we always consume one if present).
12956                let count = if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12957                    if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
12958                {
12959                    // Bare `FETCH FIRST ROW ONLY` = LIMIT 1.
12960                    crate::ast::LimitExpr::Literal(1)
12961                } else {
12962                    self.parse_limit_expr("FETCH FIRST")?
12963                };
12964                // Eat `ROW` / `ROWS` if not already consumed above.
12965                self.consume_optional_rows_keyword();
12966                // Optional `ONLY` (the spec form) — or the SQL:2008
12967                // `WITH TIES` form. v7.17.0 Phase 3.P0-49: the executor
12968                // now honours WITH TIES by extending past the LIMIT
12969                // truncation point through every row that shares the
12970                // last-kept row's ORDER BY key.
12971                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12972                    if s.eq_ignore_ascii_case("only"))
12973                {
12974                    self.advance();
12975                } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12976                    if s.eq_ignore_ascii_case("with"))
12977                {
12978                    self.advance(); // WITH
12979                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
12980                        if s.eq_ignore_ascii_case("ties"))
12981                    {
12982                        self.advance();
12983                        head.limit_with_ties = true;
12984                    }
12985                }
12986                head.limit = Some(count);
12987                saw_limit = true;
12988                continue;
12989            }
12990            break;
12991        }
12992        // v7.17.0 Phase 3.4 — trailing row-lock clauses:
12993        //   FOR { UPDATE | NO KEY UPDATE | SHARE | KEY SHARE }
12994        //       [ OF table_name [, …] ]
12995        //       [ NOWAIT | SKIP LOCKED ]
12996        // Multiple FOR clauses may stack (PG: `FOR UPDATE OF t1
12997        // FOR SHARE OF t2`). SPG is a single-writer engine — every
12998        // SELECT already returns a consistent snapshot — so these
12999        // are accept-and-discard: the parser absorbs them so
13000        // mailrs / Rails / Django code paths that emit `SELECT
13001        // … FOR UPDATE` for advisory pessimistic locking load
13002        // without a parser error. The on-disk locking model is
13003        // unchanged; callers that rely on FOR UPDATE for read-
13004        // through-write ordering still get the right answer
13005        // because SPG serialises writes anyway.
13006        head.locking = self
13007            .consume_optional_for_lock_clauses()
13008            .map(alloc::boxed::Box::new);
13009        Ok(())
13010    }
13011
13012    /// v7.17.0 Phase 3.4 — eat zero or more `FOR { UPDATE | NO KEY
13013    /// UPDATE | SHARE | KEY SHARE } [ OF tbl[, …] ] [ NOWAIT | SKIP
13014    /// LOCKED ]` trailers. Each clause is fully accepted and
13015    /// discarded — SPG's single-writer model already satisfies the
13016    /// callers' implicit ordering requirement. Stops at the first
13017    /// token that isn't `FOR`.
13018    fn consume_optional_for_lock_clauses(&mut self) -> Option<crate::ast::LockingClause> {
13019        // v7.39 (round 293, E3 Phase 1) — the clause is REPORTED now,
13020        // not discarded. PG keeps the strongest of several clauses; the
13021        // policy of the last one wins, which is what this loop records.
13022        let mut seen: Option<crate::ast::LockingClause> = None;
13023        while matches!(self.peek(), Token::For) {
13024            // v7.37.14 (A2.5-stub) — record that this query asked
13025            // for a row lock the parser is about to silently
13026            // discard. Operators surface the count via
13027            // `spg_sql::silent_for_update_count()` so they can
13028            // gauge how much of the workload depends on advisory
13029            // FOR UPDATE / FOR SHARE / FOR KEY SHARE semantics
13030            // before v7.37.15's per-row tuple locking lands.
13031            crate::record_silent_for_update_clause();
13032            self.advance(); // FOR
13033            // `NO KEY` prefix (PG) — `NO` is reserved-keyword-shaped
13034            // (`Token::Not` isn't it; PG `NO` lexes as Token::Ident).
13035            let mut no_key = false;
13036            let mut key = false;
13037            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13038                if s.eq_ignore_ascii_case("no"))
13039            {
13040                self.advance(); // NO
13041                no_key = true;
13042                // The next ident should be KEY but be generous;
13043                // anything followed by UPDATE/SHARE is accepted.
13044                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13045                    if s.eq_ignore_ascii_case("key"))
13046                {
13047                    self.advance(); // KEY
13048                }
13049            }
13050            // `KEY` prefix (PG `FOR KEY SHARE`).
13051            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13052                if s.eq_ignore_ascii_case("key"))
13053            {
13054                self.advance(); // KEY
13055                key = true;
13056            }
13057            // Lock-strength keyword: UPDATE / SHARE. Required, but
13058            // we're lenient — an unexpected token here just bails
13059            // (we already consumed FOR; caller's downstream
13060            // dispatch will error if anything actually depends on
13061            // the trailing tokens).
13062            let is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13063                if s.eq_ignore_ascii_case("update"));
13064            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13065                if s.eq_ignore_ascii_case("update") || s.eq_ignore_ascii_case("share"))
13066            {
13067                self.advance();
13068                use crate::ast::LockStrength as LS;
13069                let strength = match (is_update, no_key, key) {
13070                    (true, true, _) => LS::NoKeyUpdate,
13071                    (true, _, _) => LS::Update,
13072                    (false, _, true) => LS::KeyShare,
13073                    (false, _, _) => LS::Share,
13074                };
13075                seen = Some(crate::ast::LockingClause {
13076                    strength,
13077                    of_tables: alloc::vec::Vec::new(),
13078                    policy: crate::ast::LockWait::Wait,
13079                });
13080            } else {
13081                // FOR by itself (or `FOR KEY` with nothing after) —
13082                // give up on the lock-clause path. We've already
13083                // advanced past FOR; further attempts to parse
13084                // here would clobber state.
13085                return seen;
13086            }
13087            // Optional `OF tbl[, tbl …]`. mailrs emits this when
13088            // joining and locking only a subset of tables.
13089            if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13090                if s.eq_ignore_ascii_case("of"))
13091            {
13092                self.advance(); // OF
13093                #[allow(clippy::while_let_loop)]
13094                loop {
13095                    match self.peek() {
13096                        Token::Ident(_) | Token::QuotedIdent(_) => {
13097                            // v7.39 (round 294) — the name is CAPTURED now: PG
13098                            // validates it against the FROM clause, and an
13099                            // uncaptured list silently means "lock everything".
13100                            let mut nm = match self.advance() {
13101                                Token::Ident(n) | Token::QuotedIdent(n) => n,
13102                                _ => alloc::string::String::new(),
13103                            };
13104                            // Optional schema-qualified `schema.table`.
13105                            if matches!(self.peek(), Token::Dot) {
13106                                self.advance();
13107                                if let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone()
13108                                {
13109                                    self.advance();
13110                                    nm = n;
13111                                }
13112                            }
13113                            if let Some(c) = seen.as_mut() {
13114                                c.of_tables.push(nm);
13115                            }
13116                        }
13117                        _ => break,
13118                    }
13119                    if matches!(self.peek(), Token::Comma) {
13120                        self.advance();
13121                    } else {
13122                        break;
13123                    }
13124                }
13125            }
13126            // Optional `NOWAIT` | `SKIP LOCKED`.
13127            match self.peek().clone() {
13128                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nowait") => {
13129                    self.advance();
13130                    if let Some(c) = seen.as_mut() {
13131                        c.policy = crate::ast::LockWait::NoWait;
13132                    }
13133                }
13134                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("skip") => {
13135                    self.advance(); // SKIP
13136                    if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13137                        if s.eq_ignore_ascii_case("locked"))
13138                    {
13139                        self.advance(); // LOCKED
13140                        if let Some(c) = seen.as_mut() {
13141                            c.policy = crate::ast::LockWait::SkipLocked;
13142                        }
13143                    }
13144                }
13145                _ => {}
13146            }
13147            // Loop: PG allows multiple FOR clauses chained.
13148        }
13149        seen
13150    }
13151
13152    /// v7.9.24 — accept `LIMIT <int>` or `LIMIT $N`. mailrs H2.
13153    /// Bind value gets resolved during prepared-statement Execute;
13154    /// the Pratt expression parser would over-accept here (e.g.
13155    /// `LIMIT 5 + 5`), so we narrowly accept only the two PG forms.
13156    /// v7.17.0 Phase 5.1 — consume the `LIMIT NULL` / `LIMIT ALL`
13157    /// sentinel tokens (PG synonyms for "no limit"). Returns true
13158    /// when one was consumed; caller skips the regular
13159    /// limit-value parse and leaves `head.limit` at None.
13160    fn consume_limit_unbounded_sentinel(&mut self) -> bool {
13161        if matches!(self.peek(), Token::Null) {
13162            self.advance();
13163            return true;
13164        }
13165        if matches!(self.peek(), Token::All) {
13166            self.advance();
13167            return true;
13168        }
13169        false
13170    }
13171
13172    /// v7.17.0 Phase 5.1 — eat an optional trailing `ROW` / `ROWS`
13173    /// keyword after a LIMIT / OFFSET / FETCH FIRST value, the
13174    /// SQL-standard shape. No-op when missing.
13175    fn consume_optional_rows_keyword(&mut self) {
13176        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13177            if s.eq_ignore_ascii_case("row") || s.eq_ignore_ascii_case("rows"))
13178        {
13179            self.advance();
13180        }
13181    }
13182
13183    /// v7.39 (round 284) — `LIMIT` / `OFFSET` over a general expression.
13184    ///
13185    /// PG's row-count clause takes an `a_expr`, so `LIMIT 1+1` and
13186    /// `OFFSET 2+3` are legal; only `FETCH FIRST` is restricted to a
13187    /// constant, which is why that spelling keeps the token path below.
13188    ///
13189    /// Constants are folded here rather than carried into the tree: the
13190    /// 15+ execution paths that read the row count go through
13191    /// `limit_literal()`, which answers `Option<u32>` — and `None` there
13192    /// means "no limit". A clause the engine could not resolve would
13193    /// therefore return the WHOLE table instead of failing. Folding at
13194    /// parse time keeps that impossible; a non-constant clause is still
13195    /// a clean error (recorded residual — closing it wants a resolution
13196    /// pre-pass on the simple-query path, where `substitute_placeholders`
13197    /// does not run).
13198    fn parse_limit_expr(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13199        // PG restricts FETCH FIRST to a constant or a PARENTHESISED
13200        // expression: `FETCH FIRST 1+1 ROWS ONLY` is a syntax error, but
13201        // `FETCH FIRST (1+1) ROWS ONLY` and `FETCH FIRST (SELECT 3) ROWS
13202        // ONLY` both work (its grammar takes a c_expr). Both measured
13203        // against PG 18.4 in round 305.
13204        if label == "FETCH FIRST" && !matches!(self.peek(), Token::LParen) {
13205            return self.parse_limit_constant(label);
13206        }
13207        // One pass, no rewind: `advance()` takes each token by
13208        // `mem::replace`, so a consumed token reads back as Eof and this
13209        // parser cannot backtrack. Everything — bare literal included —
13210        // is therefore folded from the parsed expression rather than
13211        // re-read from the token stream.
13212        let start = self.pos;
13213        let e = self.parse_expr(0)?;
13214        if let crate::ast::Expr::Placeholder(n) = e {
13215            return Ok(crate::ast::LimitExpr::Placeholder(n));
13216        }
13217        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13218        match fold_limit_constant(&e) {
13219            Some(Ok(v)) if v < 0 => Err(ParseError {
13220                message: alloc::format!("{neg_label} must not be negative"),
13221                token_pos: start,
13222            }),
13223            Some(Ok(v)) => u32::try_from(v)
13224                .map(crate::ast::LimitExpr::Literal)
13225                .map_err(|_| ParseError {
13226                    message: alloc::format!("{label} value too large: {v}"),
13227                    token_pos: start,
13228                }),
13229            Some(Err(message)) => Err(ParseError {
13230                message: message.replace("{L}", neg_label),
13231                token_pos: start,
13232            }),
13233            // v7.39 (round 305, V23) — not foldable at parse time
13234            // (`LIMIT (SELECT 4)`, `LIMIT greatest(2,3)`). Carry the
13235            // expression; the engine evaluates it once before dispatch.
13236            None => Ok(crate::ast::LimitExpr::Expr(alloc::boxed::Box::new(e))),
13237        }
13238    }
13239
13240    fn parse_limit_constant(&mut self, label: &str) -> Result<crate::ast::LimitExpr, ParseError> {
13241        // v7.39 (round 239) — PG's row-count clause takes a bigint with its
13242        // coercion rules, not just an integer token: a NUMERIC rounds half
13243        // away from zero (`LIMIT 2.5` keeps 3 rows), a negative count is
13244        // refused with PG's wording ("LIMIT must not be negative", 2201W /
13245        // 2201X — FETCH FIRST shares LIMIT's), and a string coerces by its
13246        // content, failing as an input-syntax error on the value. General
13247        // expressions (`LIMIT 1+1`) stay unsupported — a recorded residual;
13248        // they need an Expr-carrying LimitExpr variant.
13249        let neg_label = if label == "OFFSET" { "OFFSET" } else { "LIMIT" };
13250        let err_at = |message: alloc::string::String, pos: usize| ParseError {
13251            message,
13252            token_pos: pos,
13253        };
13254        match self.advance() {
13255            Token::Integer(n) if n >= 0 => u32::try_from(n)
13256                .map(crate::ast::LimitExpr::Literal)
13257                .map_err(|_| ParseError {
13258                    message: alloc::format!("{label} value too large: {n}"),
13259                    token_pos: self.consumed_pos(),
13260                }),
13261            Token::Integer(_) => Err(err_at(
13262                alloc::format!("{neg_label} must not be negative"),
13263                self.pos.saturating_sub(1),
13264            )),
13265            Token::Numeric(t) => {
13266                let pos = self.pos.saturating_sub(1);
13267                let v: f64 = t.parse().map_err(|_| {
13268                    err_at(
13269                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13270                        pos,
13271                    )
13272                })?;
13273                if v < 0.0 {
13274                    return Err(err_at(
13275                        alloc::format!("{neg_label} must not be negative"),
13276                        pos,
13277                    ));
13278                }
13279                // Round half away from zero — PG's numeric→bigint cast.
13280                // (no_std: no f64::round; v is non-negative, so truncating
13281                // v + 0.5 is the same thing.)
13282                #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
13283                let rounded = (v + 0.5) as u64;
13284                u32::try_from(rounded)
13285                    .map(crate::ast::LimitExpr::Literal)
13286                    .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos))
13287            }
13288            Token::Minus => {
13289                let pos = self.pos.saturating_sub(1);
13290                match self.peek() {
13291                    Token::Integer(_) | Token::Numeric(_) => {
13292                        self.advance();
13293                        Err(err_at(
13294                            alloc::format!("{neg_label} must not be negative"),
13295                            pos,
13296                        ))
13297                    }
13298                    other => Err(err_at(
13299                        alloc::format!(
13300                            "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13301                        ),
13302                        pos,
13303                    )),
13304                }
13305            }
13306            Token::String(t) => {
13307                let pos = self.pos.saturating_sub(1);
13308                match t.trim().parse::<i64>() {
13309                    Ok(n) if n < 0 => Err(err_at(
13310                        alloc::format!("{neg_label} must not be negative"),
13311                        pos,
13312                    )),
13313                    Ok(n) => u32::try_from(n)
13314                        .map(crate::ast::LimitExpr::Literal)
13315                        .map_err(|_| err_at(alloc::format!("{label} value too large: {t}"), pos)),
13316                    Err(_) => Err(err_at(
13317                        alloc::format!("invalid input syntax for type bigint: \"{t}\""),
13318                        pos,
13319                    )),
13320                }
13321            }
13322            Token::Placeholder(n) => Ok(crate::ast::LimitExpr::Placeholder(n)),
13323            other => Err(ParseError {
13324                message: alloc::format!(
13325                    "expected non-negative integer or $N placeholder after {label}, got {other:?}"
13326                ),
13327                token_pos: self.consumed_pos(),
13328            }),
13329        }
13330    }
13331
13332    /// Parse one SELECT block without ORDER BY / LIMIT / UNION chaining —
13333    /// just `[DISTINCT] items [FROM] [WHERE] [GROUP BY]`. Returned with
13334    /// `unions` empty and `order_by` / `limit` `None`; the top-level
13335    /// `parse_select_stmt` is responsible for filling those in.
13336    /// v7.37.17 (17.6 siblings) — rewrite every `grouping(keys…)`
13337    /// call in the expression tree to the per-set integer bitmask
13338    /// (PG semantics: one bit per argument, MSB first; 1 = the key
13339    /// is dropped in this grouping set). Runs during the ROLLUP /
13340    /// CUBE / GROUPING SETS expansion, where the set is known.
13341    /// v7.39 (round 135) — collect the distinct `grouping(...)` calls appearing
13342    /// anywhere in `expr` (an ORDER BY key), without recursing into their args.
13343    fn collect_grouping_calls(expr: &Expr, out: &mut Vec<Expr>) {
13344        if let Expr::FunctionCall { name, .. } = expr
13345            && name.eq_ignore_ascii_case("grouping")
13346        {
13347            if !out.iter().any(|e| e == expr) {
13348                out.push(expr.clone());
13349            }
13350            return;
13351        }
13352        match expr {
13353            Expr::Binary { lhs, rhs, .. } => {
13354                Self::collect_grouping_calls(lhs, out);
13355                Self::collect_grouping_calls(rhs, out);
13356            }
13357            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13358                Self::collect_grouping_calls(expr, out)
13359            }
13360            Expr::FunctionCall { args, .. } => {
13361                for a in args {
13362                    Self::collect_grouping_calls(a, out);
13363                }
13364            }
13365            Expr::Case {
13366                operand,
13367                branches,
13368                else_branch,
13369            } => {
13370                if let Some(o) = operand {
13371                    Self::collect_grouping_calls(o, out);
13372                }
13373                for (c, v) in branches {
13374                    Self::collect_grouping_calls(c, out);
13375                    Self::collect_grouping_calls(v, out);
13376                }
13377                if let Some(x) = else_branch {
13378                    Self::collect_grouping_calls(x, out);
13379                }
13380            }
13381            _ => {}
13382        }
13383    }
13384
13385    /// v7.39 (round 135) — replace each `grouping(...)` call in `expr` equal to
13386    /// `grp_exprs[k]` with a reference to the synthetic ordering column
13387    /// `__grp_ord_k` (injected per grouping-set branch).
13388    fn rewrite_grouping_to_col(expr: &mut Expr, grp_exprs: &[Expr]) {
13389        if let Expr::FunctionCall { name, .. } = expr
13390            && name.eq_ignore_ascii_case("grouping")
13391        {
13392            if let Some(k) = grp_exprs.iter().position(|e| e == expr) {
13393                *expr = Expr::Column(crate::ast::ColumnName {
13394                    qualifier: None,
13395                    name: alloc::format!("__grp_ord_{k}"),
13396                });
13397            }
13398            return;
13399        }
13400        match expr {
13401            Expr::Binary { lhs, rhs, .. } => {
13402                Self::rewrite_grouping_to_col(lhs, grp_exprs);
13403                Self::rewrite_grouping_to_col(rhs, grp_exprs);
13404            }
13405            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } | Expr::IsNull { expr, .. } => {
13406                Self::rewrite_grouping_to_col(expr, grp_exprs)
13407            }
13408            Expr::FunctionCall { args, .. } => {
13409                for a in args {
13410                    Self::rewrite_grouping_to_col(a, grp_exprs);
13411                }
13412            }
13413            Expr::Case {
13414                operand,
13415                branches,
13416                else_branch,
13417            } => {
13418                if let Some(o) = operand {
13419                    Self::rewrite_grouping_to_col(o, grp_exprs);
13420                }
13421                for (c, v) in branches {
13422                    Self::rewrite_grouping_to_col(c, grp_exprs);
13423                    Self::rewrite_grouping_to_col(v, grp_exprs);
13424                }
13425                if let Some(x) = else_branch {
13426                    Self::rewrite_grouping_to_col(x, grp_exprs);
13427                }
13428            }
13429            _ => {}
13430        }
13431    }
13432
13433    /// v7.39 (round 242) — one grouping element of PG's GROUP BY grammar,
13434    /// as the list of key sets it contributes. A bare expression is one
13435    /// single-key set; `ROLLUP (u1, …, un)` the n+1 unit-prefixes (largest
13436    /// first); `CUBE` every unit-subset (largest first); `GROUPING SETS`
13437    /// the concatenation of its items' sets, where an item is itself an
13438    /// element, a parenthesized key list, or the empty set `()`. A
13439    /// ROLLUP/CUBE member in parentheses is a composite UNIT: its keys
13440    /// move together.
13441    fn parse_grouping_element(&mut self) -> Result<Vec<Vec<Expr>>, ParseError> {
13442        let is_kw = |t: &Token, kw: &str| matches!(t, Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw));
13443        // ROLLUP ( … ) / CUBE ( … )
13444        if (is_kw(self.peek(), "rollup") || is_kw(self.peek(), "cube"))
13445            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
13446        {
13447            let is_cube = is_kw(self.peek(), "cube");
13448            self.advance(); // ROLLUP / CUBE
13449            self.advance(); // (
13450            let mut units: Vec<Vec<Expr>> = Vec::new();
13451            loop {
13452                if matches!(self.peek(), Token::LParen) {
13453                    // Composite unit: (a, b) rolls up as one.
13454                    self.advance();
13455                    let mut unit = Vec::new();
13456                    if !matches!(self.peek(), Token::RParen) {
13457                        loop {
13458                            unit.push(self.parse_expr(0)?);
13459                            match self.peek() {
13460                                Token::Comma => {
13461                                    self.advance();
13462                                }
13463                                Token::RParen => break,
13464                                other => {
13465                                    return Err(self.err(format!(
13466                                        "expected ',' or ')' in grouping unit, got {other:?}"
13467                                    )));
13468                                }
13469                            }
13470                        }
13471                    }
13472                    self.advance(); // )
13473                    units.push(unit);
13474                } else {
13475                    units.push(alloc::vec![self.parse_expr(0)?]);
13476                }
13477                match self.peek() {
13478                    Token::Comma => {
13479                        self.advance();
13480                    }
13481                    Token::RParen => break,
13482                    other => {
13483                        return Err(self.err(format!(
13484                            "expected ',' or ')' in grouping list, got {other:?}"
13485                        )));
13486                    }
13487                }
13488            }
13489            self.advance(); // )
13490            let flatten = |unit_sel: &[bool]| -> Vec<Expr> {
13491                units
13492                    .iter()
13493                    .zip(unit_sel.iter())
13494                    .filter(|(_, keep)| **keep)
13495                    .flat_map(|(u, _)| u.iter().cloned())
13496                    .collect()
13497            };
13498            let n = units.len();
13499            if is_cube {
13500                let mut subsets: Vec<Vec<bool>> = (0..(1u32 << n))
13501                    .map(|mask| (0..n).map(|i| mask & (1 << i) != 0).collect())
13502                    .collect();
13503                subsets.sort_by_key(|sel| core::cmp::Reverse(sel.iter().filter(|b| **b).count()));
13504                return Ok(subsets.iter().map(|sel| flatten(sel)).collect());
13505            }
13506            return Ok((0..=n)
13507                .rev()
13508                .map(|keep| {
13509                    let sel: Vec<bool> = (0..n).map(|i| i < keep).collect();
13510                    flatten(&sel)
13511                })
13512                .collect());
13513        }
13514        // GROUPING SETS ( item [, item]* )
13515        if is_kw(self.peek(), "grouping")
13516            && matches!(self.tokens.get(self.pos + 1), Some(t) if is_kw(t, "sets"))
13517        {
13518            self.advance(); // GROUPING
13519            self.advance(); // SETS
13520            if !matches!(self.peek(), Token::LParen) {
13521                return Err(self.err(format!(
13522                    "expected '(' after GROUPING SETS, got {:?}",
13523                    self.peek()
13524                )));
13525            }
13526            self.advance(); // outer (
13527            let mut sets: Vec<Vec<Expr>> = Vec::new();
13528            loop {
13529                if matches!(self.peek(), Token::LParen) {
13530                    // A parenthesized key list (or the empty set).
13531                    self.advance();
13532                    let mut set = Vec::new();
13533                    if !matches!(self.peek(), Token::RParen) {
13534                        loop {
13535                            set.push(self.parse_expr(0)?);
13536                            match self.peek() {
13537                                Token::Comma => {
13538                                    self.advance();
13539                                }
13540                                Token::RParen => break,
13541                                other => {
13542                                    return Err(self.err(format!(
13543                                        "expected ',' or ')' in grouping set, got {other:?}"
13544                                    )));
13545                                }
13546                            }
13547                        }
13548                    }
13549                    self.advance(); // )
13550                    sets.push(set);
13551                } else {
13552                    // A nested element: ROLLUP/CUBE/GROUPING SETS or a
13553                    // bare expression.
13554                    sets.extend(self.parse_grouping_element()?);
13555                }
13556                match self.peek() {
13557                    Token::Comma => {
13558                        self.advance();
13559                    }
13560                    Token::RParen => break,
13561                    other => {
13562                        return Err(self.err(format!(
13563                            "expected ',' or ')' after a grouping set, got {other:?}"
13564                        )));
13565                    }
13566                }
13567            }
13568            self.advance(); // outer )
13569            return Ok(sets);
13570        }
13571        Ok(alloc::vec![alloc::vec![self.parse_expr(0)?]])
13572    }
13573
13574    fn substitute_grouping_calls(expr: &mut Expr, dropped: &[Expr]) {
13575        // v7.38 (read01) — a reference to a key that is dropped in this grouping
13576        // set evaluates to NULL, at any depth. Previously only a *top-level*
13577        // select item equal to a dropped key was nullified, so a key nested in
13578        // an expression (`COALESCE(g,'TOTAL')`, `g || sum(v)`) survived as a raw
13579        // column and failed to resolve against the set's synthetic schema.
13580        if dropped.iter().any(|d| d == expr) {
13581            *expr = Expr::Literal(Literal::Null);
13582            return;
13583        }
13584        if let Expr::FunctionCall { name, args } = expr
13585            && name.eq_ignore_ascii_case("grouping")
13586        {
13587            let mut mask: i64 = 0;
13588            for a in args.iter() {
13589                mask <<= 1;
13590                if dropped.iter().any(|d| d == a) {
13591                    mask |= 1;
13592                }
13593            }
13594            // v7.39 (round 242) — wrapped in a cast, NOT a bare integer
13595            // literal: a bare integer in a select item is indistinguishable
13596            // from a positional reference once `ORDER BY 1` substitutes the
13597            // item back in, and the round-232 position check then read the
13598            // mask value as an out-of-range position. The cast changes
13599            // nothing semantically (grouping() is integer).
13600            *expr = Expr::Cast {
13601                expr: alloc::boxed::Box::new(Expr::Literal(Literal::Integer(mask))),
13602                target: crate::ast::CastTarget::Int,
13603            };
13604            return;
13605        }
13606        // Generic recursion over the common expression shapes the
13607        // SELECT list uses; anything without child expressions is
13608        // left alone.
13609        match expr {
13610            // v7.40.0 — an AGGREGATE's argument is NOT nullified.
13611            //
13612            // A grouping column is NULL in the OUTPUT of a set that
13613            // drops it, and an aggregate over it still aggregates the
13614            // real values. Measured, over 0,1,2,3:
13615            //
13616            // ```text
13617            //   SELECT qty, SUM(qty) … GROUP BY ROLLUP(qty)
13618            //     PostgreSQL 18.6   the total row is  NULL | 6
13619            //     MySQL 9.7.2       the total row is  NULL | 6
13620            //     SPG 7.39.13                         NULL | NULL
13621            // ```
13622            //
13623            // The round that made this walk descend "at any depth" was
13624            // right about `COALESCE(g,'TOTAL')` and wrong about
13625            // `SUM(g)`: it turned the aggregate's own input into a NULL
13626            // literal, so the grand total of a rollup keyed on the
13627            // summed column answered nothing. Wrong on BOTH faces.
13628            //
13629            // `grouping(…)` is settled above, before this, so it keeps
13630            // reading the dropped set.
13631            Expr::FunctionCall { name, args } => {
13632                if is_aggregate_function_name(name) {
13633                    return;
13634                }
13635                for a in args {
13636                    Self::substitute_grouping_calls(a, dropped);
13637                }
13638            }
13639            Expr::AggregateOrdered { .. } => {}
13640            Expr::Binary { lhs, rhs, .. } => {
13641                Self::substitute_grouping_calls(lhs, dropped);
13642                Self::substitute_grouping_calls(rhs, dropped);
13643            }
13644            Expr::Unary { expr: inner, .. } => {
13645                Self::substitute_grouping_calls(inner, dropped);
13646            }
13647            Expr::Cast { expr: inner, .. } => {
13648                Self::substitute_grouping_calls(inner, dropped);
13649            }
13650            Expr::Case {
13651                operand,
13652                branches,
13653                else_branch,
13654            } => {
13655                if let Some(op) = operand {
13656                    Self::substitute_grouping_calls(op, dropped);
13657                }
13658                for (w, t) in branches {
13659                    Self::substitute_grouping_calls(w, dropped);
13660                    Self::substitute_grouping_calls(t, dropped);
13661                }
13662                if let Some(e) = else_branch {
13663                    Self::substitute_grouping_calls(e, dropped);
13664                }
13665            }
13666            // v7.38 (read01) — recurse into the remaining child-bearing shapes
13667            // so a dropped key nested in `IS NULL` / `LIKE` / `IN (…)` / `EXTRACT`
13668            // / a subscript / `ANY`/`ALL` is nullified too (`CASE WHEN g IS NULL
13669            // …` is the canonical rollup-total label idiom).
13670            Expr::IsNull { expr: inner, .. } => Self::substitute_grouping_calls(inner, dropped),
13671            Expr::Like { expr, pattern, .. } => {
13672                Self::substitute_grouping_calls(expr, dropped);
13673                Self::substitute_grouping_calls(pattern, dropped);
13674            }
13675            Expr::InList { expr, list, .. } => {
13676                Self::substitute_grouping_calls(expr, dropped);
13677                for item in list {
13678                    Self::substitute_grouping_calls(item, dropped);
13679                }
13680            }
13681            Expr::Extract { source, .. } => Self::substitute_grouping_calls(source, dropped),
13682            Expr::Array(items) => {
13683                for item in items {
13684                    Self::substitute_grouping_calls(item, dropped);
13685                }
13686            }
13687            Expr::ArraySubscript { target, index } => {
13688                Self::substitute_grouping_calls(target, dropped);
13689                Self::substitute_grouping_calls(index, dropped);
13690            }
13691            Expr::ArraySlice { target, lo, hi } => {
13692                Self::substitute_grouping_calls(target, dropped);
13693                if let Some(lo) = lo {
13694                    Self::substitute_grouping_calls(lo, dropped);
13695                }
13696                if let Some(hi) = hi {
13697                    Self::substitute_grouping_calls(hi, dropped);
13698                }
13699            }
13700            Expr::AnyAll { expr, array, .. } => {
13701                Self::substitute_grouping_calls(expr, dropped);
13702                Self::substitute_grouping_calls(array, dropped);
13703            }
13704            _ => {}
13705        }
13706    }
13707
13708    fn parse_bare_select(&mut self) -> Result<SelectStatement, ParseError> {
13709        // v7.40.11 — an UNPARENTHESISED `VALUES` list is a query block
13710        // too, so it can be the PEER of a set operation:
13711        //
13712        //   SELECT 1 UNION ALL VALUES (2)
13713        //
13714        // The parenthesised form has been a peer since v7.37 D.20 and
13715        // the CTE-body form since 17.6; these two unbracketed positions
13716        // were the pair nobody wrote a case for. Modern psql builds its
13717        // describe queries with `UNION ALL VALUES`, so every backslash
13718        // command — `\d`, `\dt`, `\di` — failed with a syntax error
13719        // pointing into a query the user did not write.
13720        if matches!(self.peek(), Token::Values) {
13721            self.advance(); // VALUES
13722            return self.parse_values_rows_body();
13723        }
13724        // v7.37.17 (17.6 siblings) — parenthesized set-operation
13725        // group: `( <select chain> )` usable anywhere a query block
13726        // is (head or peer of an outer chain). The group's own
13727        // unions ride the returned SelectStatement; the executor's
13728        // nested-peer recursion runs them.
13729        if matches!(self.peek(), Token::LParen)
13730            && matches!(
13731                self.tokens.get(self.pos + 1),
13732                Some(Token::Select | Token::LParen | Token::Values)
13733            )
13734        {
13735            self.advance(); // (
13736            self.enter_nested()?;
13737            // v7.37 D.20 — a group whose head is a VALUES list:
13738            // `(VALUES (1),(2)) UNION (VALUES (3))`. Parse the VALUES body,
13739            // otherwise recurse into a nested SELECT/group head.
13740            let mut head = (if matches!(self.peek(), Token::Values) {
13741                self.advance(); // VALUES
13742                self.parse_values_rows_body()
13743            } else {
13744                self.parse_bare_select()
13745            })
13746            .and_then(|mut h| {
13747                self.parse_setop_chain_into(&mut h)?;
13748                Ok(h)
13749            });
13750            self.nest_depth -= 1;
13751            let mut head = match &mut head {
13752                Ok(h) => core::mem::take(h),
13753                Err(_) => return head,
13754            };
13755            // v7.37.17 (17.6 siblings) — group-internal tail:
13756            // `(A UNION B ORDER BY 1 LIMIT 5)`. Parse it into the
13757            // group head, then wrap the group as a derived table
13758            // (SELECT * FROM (group)) so the outer chain / outer
13759            // tail can't clobber the group's own ordering or limit.
13760            let has_tail = matches!(self.peek(), Token::Order | Token::Limit | Token::Offset)
13761                || matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
13762                    if s.eq_ignore_ascii_case("fetch"));
13763            if has_tail {
13764                self.parse_select_tail_into(&mut head)?;
13765                head = SelectStatement {
13766                    locking: None,
13767                    ctes: Vec::new(),
13768                    distinct: false,
13769                    distinct_on: Vec::new(),
13770                    items: alloc::vec![SelectItem::Wildcard],
13771                    from: Some(FromClause {
13772                        primary: TableRef {
13773                            name: "subquery".to_string(),
13774                            alias: None,
13775                            only: false,
13776                            as_of_segment: None,
13777                            unnest_expr: None,
13778                            unnest_column_aliases: Vec::new(),
13779                            with_ordinality: false,
13780                            generate_series_args: None,
13781                            lateral_subquery: Some(Box::new(head)),
13782                            jsonb_each_text_arg: None,
13783                            table_fn_call: None,
13784                            rows_from: None,
13785                            json_table: None,
13786                            scalar_fn_item: false,
13787                        },
13788                        joins: Vec::new(),
13789                    }),
13790                    where_: None,
13791                    group_by: None,
13792                    group_by_all: false,
13793                    having: None,
13794                    unions: Vec::new(),
13795                    order_by: Vec::new(),
13796                    limit: None,
13797                    offset: None,
13798                    limit_with_ties: false,
13799                    window_check_exprs: Vec::new(),
13800                };
13801            }
13802            if !matches!(self.peek(), Token::RParen) {
13803                return Err(self.err(format!(
13804                    "expected ')' after parenthesized query group, got {:?}",
13805                    self.peek()
13806                )));
13807            }
13808            self.advance();
13809            return Ok(head);
13810        }
13811        // `TABLE name` shorthand as a query block — valid anywhere
13812        // a SELECT head is (set-op peers included).
13813        if matches!(self.peek(), Token::Table)
13814            && matches!(
13815                self.tokens.get(self.pos + 1),
13816                Some(Token::Ident(_) | Token::QuotedIdent(_))
13817            )
13818        {
13819            return self.parse_table_shorthand();
13820        }
13821        if !matches!(self.peek(), Token::Select) {
13822            return Err(self.err(format!(
13823                "expected SELECT to start a query block, got {:?}",
13824                self.peek()
13825            )));
13826        }
13827        self.advance();
13828        // v7.39.9 — MySQL's `SELECT STRAIGHT_JOIN …` join-order hint.
13829        //
13830        // It sits where `DISTINCT` sits and tells the optimiser to join
13831        // in the written order. SPG plans its own joins, so the hint is
13832        // accepted and not acted on — but it has to PARSE, because as a
13833        // bare identifier it became a column: measured on the published
13834        // image, `SELECT STRAIGHT_JOIN a FROM t` answered `Unknown
13835        // column 'straight_join' in 'field list'` where MySQL 9.7.2
13836        // returns the rows. Only in this position, which is the only one
13837        // MySQL accepts either — a trailing `STRAIGHT_JOIN` is its 1064.
13838        if self.mysql_dialect
13839            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("straight_join"))
13840        {
13841            self.advance();
13842        }
13843        let distinct = if matches!(self.peek(), Token::Distinct) {
13844            self.advance();
13845            true
13846        } else {
13847            false
13848        };
13849        // v7.37.17 (17.6 siblings) — `DISTINCT ON (expr [, …])`:
13850        // keep the first row (per ORDER BY) of each group the
13851        // expressions define. Django's .distinct('field') shape.
13852        let distinct_on: Vec<Expr> = if distinct && matches!(self.peek(), Token::On) {
13853            self.advance(); // ON
13854            if !matches!(self.peek(), Token::LParen) {
13855                return Err(self.err(format!(
13856                    "expected '(' after DISTINCT ON, got {:?}",
13857                    self.peek()
13858                )));
13859            }
13860            self.advance();
13861            let mut exprs = Vec::new();
13862            loop {
13863                exprs.push(self.parse_expr(0)?);
13864                match self.peek() {
13865                    Token::Comma => {
13866                        self.advance();
13867                    }
13868                    Token::RParen => break,
13869                    other => {
13870                        return Err(self.err(format!(
13871                            "expected ',' or ')' in DISTINCT ON list, got {other:?}"
13872                        )));
13873                    }
13874                }
13875            }
13876            self.advance(); // )
13877            exprs
13878        } else {
13879            Vec::new()
13880        };
13881        let mut items = self.parse_select_list()?;
13882        // v7.38.19 — `SELECT … INTO <table>`, PostgreSQL's other spelling
13883        // of CTAS. It sits exactly here in PG's grammar, right after the
13884        // target list.
13885        //
13886        // A comment in `ast.rs` has said since v7.38 that CTAS and
13887        // `SELECT INTO` lower to the same node. Only CTAS ever did:
13888        // `SELECT i INTO t FROM src` answered `syntax error at or near
13889        // "INTO"`, which the differential found while measuring what
13890        // PostgreSQL tags each of the five materialising forms with. A
13891        // comment describing a capability the code does not have is the
13892        // defect this version has been finding all day, and this is the
13893        // one it found in the parser.
13894        //
13895        // `INTO` is captured rather than consumed here: the name has to
13896        // travel out of a function that returns a `SelectStatement`, and
13897        // the caller lowers the whole thing to the CTAS node.
13898        if matches!(self.peek(), Token::Into) {
13899            self.advance();
13900            // `TEMP` / `TEMPORARY` / `UNLOGGED` / `TABLE` are modifiers on
13901            // the target, not part of its name. SPG has one storage
13902            // class, so `UNLOGGED` is accepted and means nothing, which
13903            // is what it already means on `CREATE TABLE`.
13904            let mut temporary = false;
13905            loop {
13906                match self.peek().clone() {
13907                    Token::Ident(w) | Token::QuotedIdent(w)
13908                        if w.eq_ignore_ascii_case("temp")
13909                            || w.eq_ignore_ascii_case("temporary") =>
13910                    {
13911                        temporary = true;
13912                        self.advance();
13913                    }
13914                    Token::Ident(w) | Token::QuotedIdent(w)
13915                        if w.eq_ignore_ascii_case("unlogged") =>
13916                    {
13917                        self.advance();
13918                    }
13919                    Token::Table => {
13920                        self.advance();
13921                    }
13922                    _ => break,
13923                }
13924            }
13925            let name = match self.peek().clone() {
13926                Token::Ident(w) | Token::QuotedIdent(w) => {
13927                    self.advance();
13928                    w
13929                }
13930                other => {
13931                    return Err(self.err(alloc::format!(
13932                        "expected a table name after SELECT … INTO, got {other:?}"
13933                    )));
13934                }
13935            };
13936            self.pending_select_into = Some((name, temporary));
13937        }
13938        // Scope the TABLESAMPLE lowering channel to this SELECT:
13939        // stash whatever an enclosing select accumulated, collect
13940        // our own FROM's predicates, restore after the combine.
13941        let enclosing_sample_preds = core::mem::take(&mut self.pending_sample_preds);
13942        let mut from = if matches!(self.peek(), Token::From) {
13943            self.advance();
13944            Some(self.parse_from_clause()?)
13945        } else {
13946            None
13947        };
13948        // v7.37 D.22 — a set-returning function in the projection with no FROM
13949        // (`SELECT unnest(arr)`, `SELECT 'x', generate_series(a,b)`) expands to
13950        // rows. Move the first SRF projection item to a FROM-position derived
13951        // table and replace it in the projection with a reference to its output
13952        // column; sibling scalar columns repeat per SRF row. PG names the output
13953        // column after the function (or its AS alias). Reuses the FROM-SRF
13954        // machinery. Only fires with no FROM — mixed SRF over a real FROM already
13955        // works via the targetlist-SRF path.
13956        // v7.39 (read01 round 69) — lower `(f(args)).*`. With no FROM it IS
13957        // `SELECT * FROM f(args)` — the record's fields become the columns, which
13958        // is exactly what the function's own row shape already is. Anywhere else
13959        // (per outer row, or beside other items) it would need a real record-typed
13960        // projection, so it says so rather than answering something else.
13961        if let [
13962            SelectItem::Expr {
13963                expr: Expr::FunctionCall { name, args },
13964                ..
13965            },
13966        ] = items.as_slice()
13967            && name == "__record_expand"
13968        {
13969            let Some(Expr::FunctionCall {
13970                name: inner_name,
13971                args: inner_args,
13972            }) = args.first()
13973            else {
13974                return Err(self.err(
13975                    "(<expr>).* expands a function's record — it needs a function call".into(),
13976                ));
13977            };
13978            if from.is_some() {
13979                return Err(self.err(
13980                    "(<fn>).* over a FROM clause is not supported — call the function in FROM"
13981                        .into(),
13982                ));
13983            }
13984            let fn_ref = TableRef {
13985                name: inner_name.clone(),
13986                alias: None,
13987                only: false,
13988                as_of_segment: None,
13989                unnest_expr: None,
13990                unnest_column_aliases: Vec::new(),
13991                with_ordinality: false,
13992                generate_series_args: None,
13993                lateral_subquery: None,
13994                jsonb_each_text_arg: None,
13995                table_fn_call: Some(Box::new((
13996                    inner_name.to_ascii_lowercase(),
13997                    inner_args.clone(),
13998                ))),
13999                rows_from: None,
14000                json_table: None,
14001                scalar_fn_item: false,
14002            };
14003            items = alloc::vec![SelectItem::Wildcard];
14004            from = Some(FromClause {
14005                primary: fn_ref,
14006                joins: Vec::new(),
14007            });
14008        }
14009        // v7.39 (read01 round 74) — `(f(args)).*` beside other items, or over a
14010        // FROM, keeps its marker: the ENGINE lowers it, because naming the
14011        // record's fields takes the catalog. It becomes a LATERAL of the same
14012        // function plus one item per declared column — the machinery rounds 65
14013        // and 69 already built.
14014        // v7.39 (read01 round 67) — the lift moves ONE SRF into FROM. With two
14015        // (`SELECT generate_series(1,3), generate_series(10,11)`) PG runs them in
14016        // LOCKSTEP, padding the shorter with NULLs — a shape the lift cannot
14017        // express, since the lifted one becomes a scan and the other would
14018        // expand per its rows (a cross product, not a zip). So when the
14019        // projection holds more than one top-level function call, the lift steps
14020        // aside and the engine's target-list expansion takes the whole list.
14021        let fn_call_items = items
14022            .iter()
14023            .filter(|it| {
14024                matches!(
14025                    it,
14026                    SelectItem::Expr {
14027                        expr: Expr::FunctionCall { .. },
14028                        ..
14029                    }
14030                )
14031            })
14032            .count();
14033        if from.is_none() && fn_call_items <= 1 {
14034            let mut found: Option<(usize, TableRef, String)> = None;
14035            for (i, item) in items.iter().enumerate() {
14036                if let SelectItem::Expr {
14037                    expr: Expr::FunctionCall { name, args },
14038                    alias,
14039                } = item
14040                {
14041                    let lname = name.to_ascii_lowercase();
14042                    let colname = alias.clone().unwrap_or_else(|| lname.clone());
14043                    let (unnest, gs) = match lname.as_str() {
14044                        "unnest" if args.len() == 1 => (Some(Box::new(args[0].clone())), None),
14045                        "generate_series" if (2..=3).contains(&args.len()) => {
14046                            (None, Some(args.clone()))
14047                        }
14048                        // v7.38 (read01) — generate_subscripts(arr, dim) in a
14049                        // no-FROM projection yields the 1-based subscripts, i.e.
14050                        // generate_series(1, array_length(arr, dim)); an invalid
14051                        // dimension makes array_length NULL → 0 rows, as in PG.
14052                        "generate_subscripts" if args.len() == 2 => (
14053                            None,
14054                            Some(alloc::vec![
14055                                Expr::Literal(Literal::Integer(1)),
14056                                Expr::FunctionCall {
14057                                    name: "array_length".to_string(),
14058                                    args: args.clone(),
14059                                },
14060                            ]),
14061                        ),
14062                        // v7.38 (read01, T-srf) — string_to_table / regexp_split_to_table
14063                        // in a no-FROM projection unnest their *_to_array form.
14064                        "string_to_table" | "regexp_split_to_table" => {
14065                            let array_fn = if lname == "string_to_table" {
14066                                "string_to_array"
14067                            } else {
14068                                "regexp_split_to_array"
14069                            };
14070                            (
14071                                Some(Box::new(Expr::FunctionCall {
14072                                    name: array_fn.to_string(),
14073                                    args: args.clone(),
14074                                })),
14075                                None,
14076                            )
14077                        }
14078                        // v7.38 (read01, T15) — jsonb/json_array_elements[_text] in
14079                        // a no-FROM projection expand per element. The scalar form
14080                        // returns the elements as a TEXT array, so unnest over the
14081                        // same call materialises one row each (same rewrite the
14082                        // FROM-clause form uses).
14083                        "jsonb_array_elements"
14084                        | "json_array_elements"
14085                        | "jsonb_array_elements_text"
14086                        | "json_array_elements_text"
14087                            if args.len() == 1 =>
14088                        {
14089                            (
14090                                Some(Box::new(Expr::FunctionCall {
14091                                    name: lname.clone(),
14092                                    args: args.clone(),
14093                                })),
14094                                None,
14095                            )
14096                        }
14097                        // v7.38 (read01, T15) — jsonb/json_path_query(doc, path)
14098                        // in a no-FROM projection expands per match (scalar form
14099                        // returns the matches as a TEXT array → unnest).
14100                        "jsonb_path_query" | "json_path_query" if args.len() == 2 => (
14101                            Some(Box::new(Expr::FunctionCall {
14102                                name: lname.clone(),
14103                                args: args.clone(),
14104                            })),
14105                            None,
14106                        ),
14107                        _ => continue,
14108                    };
14109                    found = Some((
14110                        i,
14111                        TableRef {
14112                            name: colname.clone(),
14113                            alias: Some(colname.clone()),
14114                            only: false,
14115                            as_of_segment: None,
14116                            unnest_expr: unnest,
14117                            unnest_column_aliases: alloc::vec![colname.clone()],
14118                            with_ordinality: false,
14119                            generate_series_args: gs,
14120                            lateral_subquery: None,
14121                            jsonb_each_text_arg: None,
14122                            table_fn_call: None,
14123                            rows_from: None,
14124                            json_table: None,
14125                            scalar_fn_item: false,
14126                        },
14127                        colname,
14128                    ));
14129                    break;
14130                }
14131            }
14132            if let Some((idx, tref, colname)) = found {
14133                from = Some(FromClause {
14134                    primary: tref,
14135                    joins: Vec::new(),
14136                });
14137                items[idx] = SelectItem::Expr {
14138                    expr: Expr::Column(ColumnName {
14139                        qualifier: None,
14140                        name: colname.clone(),
14141                    }),
14142                    alias: Some(colname),
14143                };
14144            }
14145        }
14146        let sample_preds = core::mem::take(&mut self.pending_sample_preds);
14147        let where_ = if matches!(self.peek(), Token::Where) {
14148            self.advance();
14149            Some(self.parse_expr(0)?)
14150        } else {
14151            None
14152        };
14153        let where_ = sample_preds.into_iter().fold(where_, |acc, pred| {
14154            Some(match acc {
14155                Some(w) => Expr::Binary {
14156                    lhs: Box::new(pred),
14157                    op: crate::ast::BinOp::And,
14158                    rhs: Box::new(w),
14159                },
14160                None => pred,
14161            })
14162        });
14163        self.pending_sample_preds = enclosing_sample_preds;
14164        let mut group_by_all = false;
14165        // v7.37.17 (17.6 siblings) — ROLLUP / CUBE / GROUPING SETS
14166        // share one expansion: `grouping_sets` lists the key subsets
14167        // (first = primary, assigned to stmt.group_by; the rest
14168        // become UNION ALL peers), `grouping_universe` is the full
14169        // key list used to compute each peer's dropped keys.
14170        let mut grouping_sets: Vec<Vec<Expr>> = Vec::new();
14171        let mut grouping_universe: Vec<Expr> = Vec::new();
14172        // v7.39 (round 472) — did the GROUP BY end in MySQL's `WITH ROLLUP`?
14173        // A BOOL, not the key list: this frame is the statement parser's, and
14174        // round 430 measured that a `Vec` local here is enough on its own to
14175        // tip the 512 KiB nesting guard. The keys are recoverable from
14176        // `grouping_universe`, which a rollup fills with exactly them.
14177        let mut mysql_rollup = false;
14178        let group_by = if matches!(self.peek(), Token::Group) {
14179            self.advance();
14180            if !self.peek_is_by() {
14181                return Err(self.err(format!("expected BY after GROUP, got {:?}", self.peek())));
14182            }
14183            self.advance();
14184            // v6.4.1 — `GROUP BY ALL` shortcut. Planner expands to
14185            // every non-aggregate SELECT-list item later.
14186            if matches!(self.peek(), Token::All) {
14187                self.advance();
14188                group_by_all = true;
14189                None
14190            } else {
14191                // v7.39 (round 242) — PG's general grouping-element grammar:
14192                // GROUP BY [DISTINCT] element [, element]*, where an element
14193                // is a bare expression, ROLLUP (…), CUBE (…) or GROUPING
14194                // SETS (…) — mixed freely. Each element yields a list of
14195                // key sets; the query's grouping sets are the CARTESIAN
14196                // PRODUCT of the elements' lists (so `a, ROLLUP (b)` is
14197                // {(a,b),(a)}), and DISTINCT drops duplicate sets by
14198                // content. ROLLUP/CUBE members may be composite
14199                // (`ROLLUP ((a, b))` moves a and b as one unit), and a
14200                // GROUPING SETS item may itself be a ROLLUP/CUBE. The old
14201                // parser handled only a lone ROLLUP/CUBE/GS as the whole
14202                // clause.
14203                let distinct_sets = if matches!(self.peek(), Token::Distinct) {
14204                    self.advance();
14205                    true
14206                } else {
14207                    false
14208                };
14209                let mut element_sets: Vec<Vec<Vec<Expr>>> = Vec::new();
14210                loop {
14211                    element_sets.push(self.parse_grouping_element()?);
14212                    if matches!(self.peek(), Token::Comma) {
14213                        self.advance();
14214                    } else {
14215                        break;
14216                    }
14217                }
14218                let mut total: Vec<Vec<Expr>> = alloc::vec![Vec::new()];
14219                for el in &element_sets {
14220                    let mut next: Vec<Vec<Expr>> = Vec::new();
14221                    for base in &total {
14222                        for set in el {
14223                            let mut merged = base.clone();
14224                            for k in set {
14225                                if !merged.iter().any(|m| m == k) {
14226                                    merged.push(k.clone());
14227                                }
14228                            }
14229                            next.push(merged);
14230                        }
14231                    }
14232                    total = next;
14233                }
14234                // v7.39 (round 472) — MySQL spells a rollup as a SUFFIX:
14235                // `GROUP BY a, b WITH ROLLUP` is PG's `GROUP BY ROLLUP(a, b)`.
14236                // The keys and the aggregates come out identical; the ROW
14237                // ORDER does not, and that is the part a report depends on.
14238                // MySQL interleaves each group's subtotal right after its
14239                // own rows (east/a, east/b, east/NULL, west/a, …, NULL/NULL)
14240                // where the union-of-grouping-sets expansion emits every
14241                // leaf first and then every subtotal. MariaDB REFUSES an
14242                // ORDER BY next to ROLLUP (1221), so a client cannot fix the
14243                // order itself — measured on MariaDB 11 and MySQL 9.7, which
14244                // agree on the order and disagree only on whether ORDER BY
14245                // is allowed (MySQL allows it; SPG allows it too, since
14246                // refusing would break the clients that can write it).
14247                if self.mysql_dialect
14248                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"))
14249                    && matches!(
14250                        self.tokens.get(self.pos + 1),
14251                        Some(Token::Ident(r)) if r.eq_ignore_ascii_case("rollup")
14252                    )
14253                {
14254                    self.advance(); // WITH
14255                    self.advance(); // ROLLUP
14256                    let keys = total.into_iter().next().unwrap_or_default();
14257                    mysql_rollup = true;
14258                    // n+1 prefixes, largest first — the same expansion
14259                    // `ROLLUP (…)` produces.
14260                    total = (0..=keys.len()).rev().map(|n| keys[..n].to_vec()).collect();
14261                }
14262                if distinct_sets {
14263                    let mut seen: Vec<Vec<String>> = Vec::new();
14264                    total.retain(|set| {
14265                        let mut key: Vec<String> =
14266                            set.iter().map(|e| alloc::format!("{e}")).collect();
14267                        key.sort();
14268                        if seen.contains(&key) {
14269                            false
14270                        } else {
14271                            seen.push(key);
14272                            true
14273                        }
14274                    });
14275                }
14276                if total.len() > 1 {
14277                    let mut universe: Vec<Expr> = Vec::new();
14278                    for set in &total {
14279                        for k in set {
14280                            if !universe.iter().any(|u| u == k) {
14281                                universe.push(k.clone());
14282                            }
14283                        }
14284                    }
14285                    grouping_universe = universe;
14286                    let primary = total[0].clone();
14287                    grouping_sets = total;
14288                    Some(primary)
14289                } else {
14290                    // One set (a plain GROUP BY list, or a single-set
14291                    // spelling like GROUPING SETS ((a, b))). An EMPTY
14292                    // single set — GROUPING SETS (()) — stays
14293                    // `Some(vec![])`: the grand-total group, which must
14294                    // run the aggregate path.
14295                    Some(total.into_iter().next().unwrap_or_default())
14296                }
14297            }
14298        } else {
14299            None
14300        };
14301        let having = if matches!(self.peek(), Token::Having) {
14302            self.advance();
14303            Some(self.parse_expr(0)?)
14304        } else {
14305            None
14306        };
14307        // `WINDOW w AS ( <window-def> ) [, ...]` — named windows.
14308        // OVER w parsed to a marker above; inline each definition
14309        // into the referencing WindowFunction nodes.
14310        let mut window_defs: Vec<(String, WindowDef)> = Vec::new();
14311        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("window")) {
14312            self.advance();
14313            loop {
14314                let wname = self.expect_ident_like()?;
14315                if !matches!(self.peek(), Token::As) {
14316                    return Err(self.err(format!(
14317                        "expected AS after WINDOW {wname}, got {:?}",
14318                        self.peek()
14319                    )));
14320                }
14321                self.advance();
14322                // v7.39 (round 229) — PG rejects a redefinition outright.
14323                if window_defs
14324                    .iter()
14325                    .any(|(n, _)| n.eq_ignore_ascii_case(&wname))
14326                {
14327                    return Err(self.err(alloc::format!("window \"{wname}\" is already defined")));
14328                }
14329                let def = self.parse_over_clause()?;
14330                // A definition may itself copy an earlier one
14331                // (`WINDOW w1 AS (PARTITION BY g), w2 AS (w1 ORDER BY v)`),
14332                // so resolve it against the defs already in scope. Same
14333                // copy rules as an `OVER (w1 …)` in the select list.
14334                let mut probe = Expr::WindowFunction {
14335                    name: String::new(),
14336                    args: Vec::new(),
14337                    partition_by: def.0,
14338                    order_by: def.1,
14339                    frame: def.2,
14340                    null_treatment: crate::ast::NullTreatment::Respect,
14341                    filter: None,
14342                };
14343                Self::substitute_named_windows(&mut probe, &window_defs)
14344                    .map_err(|m| self.err(m))?;
14345                let Expr::WindowFunction {
14346                    partition_by,
14347                    order_by,
14348                    frame,
14349                    ..
14350                } = probe
14351                else {
14352                    unreachable!("probe is a WindowFunction")
14353                };
14354                window_defs.push((wname, (partition_by, order_by, frame)));
14355                if matches!(self.peek(), Token::Comma) {
14356                    self.advance();
14357                    continue;
14358                }
14359                break;
14360            }
14361        }
14362        // v7.39 (round 705) — which definitions did anything reference?
14363        // The ones nothing did used to be dropped here, unexamined, so
14364        // `WINDOW w AS (ORDER BY nosuch)` succeeded — PG analyses every
14365        // definition whether referenced or not. Their key expressions ride
14366        // out on the statement for the engine to resolve.
14367        let mut window_refs: Vec<String> = Vec::new();
14368        if !window_defs.is_empty() {
14369            for it in &items {
14370                if let SelectItem::Expr { expr, .. } = it {
14371                    Self::collect_named_window_refs(expr, &mut window_refs);
14372                }
14373            }
14374        }
14375        let window_check_exprs: Vec<Expr> = window_defs
14376            .iter()
14377            .filter(|(n, _)| !window_refs.iter().any(|r| r.eq_ignore_ascii_case(n)))
14378            .flat_map(|(_, (partition, order, _))| {
14379                partition
14380                    .iter()
14381                    .cloned()
14382                    .chain(order.iter().map(|(e, _, _)| e.clone()))
14383            })
14384            .collect();
14385        if !window_defs.is_empty()
14386            || items
14387                .iter()
14388                .any(|it| matches!(it, SelectItem::Expr { expr, .. } if Self::expr_has_named_window(expr)))
14389        {
14390            for it in &mut items {
14391                if let SelectItem::Expr { expr, .. } = it {
14392                    Self::substitute_named_windows(expr, &window_defs)
14393                        .map_err(|m| self.err(m))?;
14394                }
14395            }
14396        }
14397        // `GROUP BY 1` — positional keys substitute with the Nth
14398        // select item's expression (same contract ORDER BY has had
14399        // since v6.x). Out-of-range positions error.
14400        let group_by = match group_by {
14401            Some(mut keys) => {
14402                for k in &mut keys {
14403                    if let Expr::Literal(Literal::Integer(n)) = k {
14404                        let idx = *n;
14405                        if idx < 1 || idx as usize > items.len() {
14406                            return Err(self.err(alloc::format!(
14407                                "GROUP BY position {idx} is not in select list"
14408                            )));
14409                        }
14410                        match &items[(idx - 1) as usize] {
14411                            SelectItem::Expr { expr, .. } => *k = expr.clone(),
14412                            SelectItem::Wildcard | SelectItem::QualifiedWildcard(_) => {
14413                                return Err(self.err(alloc::format!(
14414                                    "GROUP BY position {idx} references a wildcard item"
14415                                )));
14416                            }
14417                        }
14418                    }
14419                }
14420                Some(keys)
14421            }
14422            None => None,
14423        };
14424        let mut stmt = SelectStatement {
14425            locking: None,
14426            ctes: Vec::new(),
14427            distinct,
14428            distinct_on,
14429            items,
14430            from,
14431            where_,
14432            group_by,
14433            group_by_all,
14434            having,
14435            unions: Vec::new(),
14436            order_by: Vec::new(),
14437            limit: None,
14438            offset: None,
14439            limit_with_ties: false,
14440            window_check_exprs,
14441        };
14442        // Grouping expansion (ROLLUP / CUBE / GROUPING SETS): the
14443        // first set is the primary (already on stmt.group_by); each
14444        // further set becomes a UNION ALL peer with its dropped
14445        // keys (universe minus the set) replaced by NULL literals
14446        // in the peer's items and group_by. PG-legal: non-grouped
14447        // select items must be group keys or aggregates, so a
14448        // dropped key's occurrences in the projection are exactly
14449        // the ones to nullify.
14450        // v7.39 (round 242) — grouping() OUTSIDE an expansion: PG allows it
14451        // over a plain GROUP BY (every argument must be a group key; the
14452        // mask is then 0) and rejects anything else with 42803. SPG's
14453        // rewrite only ran during the ROLLUP/CUBE expansion, so a plain
14454        // `SELECT grouping(a) … GROUP BY a` died at eval with "unknown
14455        // function `grouping`".
14456        if grouping_sets.len() <= 1 {
14457            let keys: Vec<Expr> = stmt.group_by.clone().unwrap_or_default();
14458            let mut calls: Vec<Expr> = Vec::new();
14459            for item in &stmt.items {
14460                if let SelectItem::Expr { expr, .. } = item {
14461                    Self::collect_grouping_calls(expr, &mut calls);
14462                }
14463            }
14464            if let Some(h) = &stmt.having {
14465                Self::collect_grouping_calls(h, &mut calls);
14466            }
14467            for call in &calls {
14468                let Expr::FunctionCall { args, .. } = call else {
14469                    continue;
14470                };
14471                for a in args {
14472                    if !keys.iter().any(|k| k == a) {
14473                        return Err(self.err(
14474                            "arguments to GROUPING must be grouping expressions of the associated query level"
14475                                .to_string(),
14476                        ));
14477                    }
14478                }
14479            }
14480            if !calls.is_empty() {
14481                for item in &mut stmt.items {
14482                    if let SelectItem::Expr { expr, .. } = item {
14483                        Self::substitute_grouping_calls(expr, &[]);
14484                    }
14485                }
14486                if let Some(h) = &mut stmt.having {
14487                    Self::substitute_grouping_calls(h, &[]);
14488                }
14489            }
14490        }
14491        if grouping_sets.len() > 1 {
14492            // The primary set's own dropped keys nullify in the
14493            // HEAD's projection too (GROUPING SETS's first set may
14494            // omit keys other sets use).
14495            let primary = grouping_sets[0].clone();
14496            let head_dropped: Vec<Expr> = grouping_universe
14497                .iter()
14498                .filter(|u| !primary.iter().any(|k| k == *u))
14499                .cloned()
14500                .collect();
14501            for set in grouping_sets.iter().skip(1) {
14502                let mut peer = stmt.clone();
14503                peer.unions = Vec::new();
14504                let dropped: Vec<&Expr> = grouping_universe
14505                    .iter()
14506                    .filter(|u| !set.iter().any(|k| k == *u))
14507                    .collect();
14508                // Empty set = grand-total group: `Some(vec![])` forces
14509                // the aggregate path (one collapsed row) instead of a
14510                // per-row passthrough. See the primary-set note above.
14511                peer.group_by = Some(set.clone());
14512                let dropped_owned: Vec<Expr> = dropped.iter().map(|d| (*d).clone()).collect();
14513                for item in &mut peer.items {
14514                    if let SelectItem::Expr { expr, alias } = item {
14515                        if dropped.iter().any(|d| *d == expr) {
14516                            // v7.39 — keep the dropped key's name on the
14517                            // NULL literal so the UNION output column
14518                            // (and any top-level ORDER BY on it) still
14519                            // resolves.
14520                            if alias.is_none()
14521                                && let Expr::Column(c) = &expr
14522                            {
14523                                *alias = Some(c.name.clone());
14524                            }
14525                            *expr = Expr::Literal(Literal::Null);
14526                        } else {
14527                            Self::substitute_grouping_calls(expr, &dropped_owned);
14528                        }
14529                    }
14530                }
14531                if let Some(h) = &mut peer.having {
14532                    Self::substitute_grouping_calls(h, &dropped_owned);
14533                }
14534                stmt.unions.push((UnionKind::All, peer));
14535            }
14536            for item in &mut stmt.items {
14537                if let SelectItem::Expr { expr, alias } = item {
14538                    if head_dropped.iter().any(|d| d == expr) {
14539                        if alias.is_none()
14540                            && let Expr::Column(c) = &expr
14541                        {
14542                            *alias = Some(c.name.clone());
14543                        }
14544                        *expr = Expr::Literal(Literal::Null);
14545                    } else {
14546                        Self::substitute_grouping_calls(expr, &head_dropped);
14547                    }
14548                }
14549            }
14550            if let Some(h) = &mut stmt.having {
14551                Self::substitute_grouping_calls(h, &head_dropped);
14552            }
14553            // v7.39 (round 135) — GROUPING() in ORDER BY. Parse the ORDER BY now
14554            // (while `grouping_universe` / the per-branch sets are in scope). For
14555            // each grouping() call in it, inject a per-branch hidden column
14556            // `__grp_ord_K` carrying that branch's mask into the head + every
14557            // peer, and rewrite the ORDER BY to reference it. `parse_select_tail_into`
14558            // preserves this pre-set order_by; the engine strips `__grp_ord_*`
14559            // from the final output. A standalone grouping-set query has ORDER BY
14560            // (not an explicit set-op) next, so consuming it here is safe.
14561            // v7.39 (round 472) — absent the client's own ORDER BY, a MySQL
14562            // rollup carries the hierarchical order: sort by the grouping
14563            // keys with the rolled-up NULLs last, which is exactly the
14564            // interleaving both oracles emit. A client's own ORDER BY wins,
14565            // which is what MySQL does (MariaDB refuses to let one be
14566            // written at all).
14567            // The synthesised keys have to travel the SAME path a written
14568            // ORDER BY does: the block below is what turns a `grouping()`
14569            // call into the per-branch `__grp_ord_K` column the engine can
14570            // actually sort on. Bypassing it left a bare `grouping(text)`
14571            // for the evaluator to reject.
14572            let synthesised_or_parsed: Vec<OrderBy> = if matches!(self.peek(), Token::Order) {
14573                self.parse_order_by_keys()?
14574            } else if mysql_rollup {
14575                Self::mysql_rollup_order(&grouping_universe)
14576            } else {
14577                Vec::new()
14578            };
14579            if !synthesised_or_parsed.is_empty() {
14580                let mut order_keys = synthesised_or_parsed;
14581                let mut grp_exprs: Vec<Expr> = Vec::new();
14582                for ob in &order_keys {
14583                    Self::collect_grouping_calls(&ob.expr, &mut grp_exprs);
14584                }
14585                for (k, gexpr) in grp_exprs.iter().enumerate() {
14586                    let colname = alloc::format!("__grp_ord_{k}");
14587                    // Head branch (primary set) uses `head_dropped`.
14588                    let mut he = gexpr.clone();
14589                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14590                    stmt.items.push(SelectItem::Expr {
14591                        expr: he,
14592                        alias: Some(colname.clone()),
14593                    });
14594                    // Each peer `stmt.unions[i]` corresponds to `grouping_sets[i+1]`.
14595                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14596                        let set = &grouping_sets[i + 1];
14597                        let dropped: Vec<Expr> = grouping_universe
14598                            .iter()
14599                            .filter(|u| !set.iter().any(|k| k == *u))
14600                            .cloned()
14601                            .collect();
14602                        let mut pe = gexpr.clone();
14603                        Self::substitute_grouping_calls(&mut pe, &dropped);
14604                        peer.items.push(SelectItem::Expr {
14605                            expr: pe,
14606                            alias: Some(colname.clone()),
14607                        });
14608                    }
14609                }
14610                for ob in &mut order_keys {
14611                    Self::rewrite_grouping_to_col(&mut ob.expr, &grp_exprs);
14612                }
14613                // v7.40.0 — and a KEY the order sorts on that the query
14614                // did not project.
14615                //
14616                // A UNION's ORDER BY can only name output columns, so
14617                // the rollup order synthesised over `grouping_universe`
14618                // named `qty` for `SELECT SUM(qty) … GROUP BY qty WITH
14619                // ROLLUP` and the query answered `column "qty" does not
14620                // exist`. MySQL 9.7.2 answers 0, 1, 2, 3, 6 — it orders
14621                // by the key whether or not it is selected. The key
14622                // travels as a hidden column, exactly as the grouping
14623                // mask above does, and is stripped from the output by
14624                // the same rule.
14625                let mut key_exprs: Vec<Expr> = Vec::new();
14626                for ob in &order_keys {
14627                    let is_key = grouping_universe.iter().any(|u| u == &ob.expr);
14628                    let projected = stmt
14629                        .items
14630                        .iter()
14631                        .any(|it| matches!(it, SelectItem::Expr { expr, .. } if expr == &ob.expr));
14632                    if is_key && !projected && !key_exprs.iter().any(|k| k == &ob.expr) {
14633                        key_exprs.push(ob.expr.clone());
14634                    }
14635                }
14636                for (k, kexpr) in key_exprs.iter().enumerate() {
14637                    let colname = alloc::format!("__grp_key_{k}");
14638                    let mut he = kexpr.clone();
14639                    Self::substitute_grouping_calls(&mut he, &head_dropped);
14640                    stmt.items.push(SelectItem::Expr {
14641                        expr: he,
14642                        alias: Some(colname.clone()),
14643                    });
14644                    for (i, (_, peer)) in stmt.unions.iter_mut().enumerate() {
14645                        let set = &grouping_sets[i + 1];
14646                        let dropped: Vec<Expr> = grouping_universe
14647                            .iter()
14648                            .filter(|u| !set.iter().any(|kk| kk == *u))
14649                            .cloned()
14650                            .collect();
14651                        let mut pe = kexpr.clone();
14652                        Self::substitute_grouping_calls(&mut pe, &dropped);
14653                        peer.items.push(SelectItem::Expr {
14654                            expr: pe,
14655                            alias: Some(colname.clone()),
14656                        });
14657                    }
14658                    for ob in &mut order_keys {
14659                        if &ob.expr == kexpr {
14660                            ob.expr = Expr::Column(crate::ast::ColumnName {
14661                                name: colname.clone(),
14662                                qualifier: None,
14663                            });
14664                        }
14665                    }
14666                }
14667                stmt.order_by = order_keys;
14668            }
14669        }
14670        Ok(stmt)
14671    }
14672
14673    /// v7.39 (round 472) — the row order MySQL's `WITH ROLLUP` promises,
14674    /// as ORDER BY keys.
14675    ///
14676    /// Per key: the rollup marker, then the key. Sorting on the key alone
14677    /// is not enough, and a table with a NULL in it says why — MariaDB puts
14678    /// the DATA-NULL group where a plain GROUP BY puts it (first) and only
14679    /// the ROLLUP-introduced NULL last, and both print as NULL.
14680    /// `GROUPING(k)` is the one thing that tells them apart: 0 for every
14681    /// real group including the data-NULL one, 1 only for the row the
14682    /// rollup added. Measured on MariaDB 11 — `('a',1),(NULL,2),('b',3)`
14683    /// rolls up to NULL|2, a|1, b|3, NULL|6.
14684    ///
14685    /// `#[inline(never)]`: its locals must not join the statement parser's
14686    /// frame, which round 430 measured sitting against the nesting guard.
14687    #[inline(never)]
14688    fn mysql_rollup_order(keys: &[Expr]) -> Vec<OrderBy> {
14689        let mut out: Vec<OrderBy> = Vec::with_capacity(keys.len() * 2);
14690        for e in keys {
14691            out.push(OrderBy {
14692                expr: Expr::FunctionCall {
14693                    name: "grouping".into(),
14694                    args: alloc::vec![e.clone()],
14695                },
14696                desc: false,
14697                nulls_first: None,
14698                collation: None,
14699            });
14700            out.push(OrderBy {
14701                expr: e.clone(),
14702                desc: false,
14703                // MySQL orders NULL first on an ascending key.
14704                nulls_first: Some(true),
14705                collation: None,
14706            });
14707        }
14708        out
14709    }
14710
14711    /// v7.39 (round 535) — `REINDEX [(opts)] { INDEX | TABLE | SCHEMA |
14712    /// DATABASE | SYSTEM } [CONCURRENTLY] [<name>]`.
14713    #[inline(never)]
14714    fn parse_reindex_tail(&mut self) -> Result<Statement, ParseError> {
14715        use crate::ast::MaintainKind;
14716        self.skip_paren_option_list();
14717        let kind = match self.peek() {
14718            // `TABLE` and `INDEX` lex as keywords, not identifiers.
14719            Token::Table | Token::Index => {
14720                self.advance();
14721                MaintainKind::ReindexRelation
14722            }
14723            Token::Ident(s) | Token::QuotedIdent(s) => match s.to_ascii_lowercase().as_str() {
14724                "index" | "table" => {
14725                    self.advance();
14726                    MaintainKind::ReindexRelation
14727                }
14728                "schema" => {
14729                    self.advance();
14730                    MaintainKind::ReindexSchema
14731                }
14732                "system" | "database" => {
14733                    self.advance();
14734                    MaintainKind::Whole
14735                }
14736                // PG requires the object type; anything else is the
14737                // caller's problem, not something to swallow.
14738                _ => MaintainKind::ReindexRelation,
14739            },
14740            _ => MaintainKind::Whole,
14741        };
14742        // PG bars `REINDEX … CONCURRENTLY` inside a transaction block and
14743        // allows the plain form, so the modifier is recorded rather than
14744        // skipped. It still has no effect on how the reindex runs.
14745        let mut concurrently = false;
14746        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("concurrently")) {
14747            self.advance();
14748            concurrently = true;
14749        }
14750        let target = self.take_optional_maintain_name();
14751        self.consume_until_statement_boundary();
14752        Ok(Statement::Maintain {
14753            kind,
14754            concurrently,
14755            target,
14756        })
14757    }
14758
14759    /// v7.39 (round 535) — `CLUSTER [VERBOSE] [<table> [USING <index>]]`
14760    /// and `CLUSTER [VERBOSE] <index> ON <table>`.
14761    #[inline(never)]
14762    fn parse_cluster_tail(&mut self) -> Result<Statement, ParseError> {
14763        use crate::ast::MaintainKind;
14764        self.skip_paren_option_list();
14765        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("verbose")) {
14766            self.advance();
14767        }
14768        let target = self.take_optional_maintain_name();
14769        self.consume_until_statement_boundary();
14770        Ok(Statement::Maintain {
14771            kind: if target.is_some() {
14772                MaintainKind::ClusterRelation
14773            } else {
14774                MaintainKind::Whole
14775            },
14776            // CLUSTER has no CONCURRENTLY form, and PG runs it inside a
14777            // transaction block quite happily (measured).
14778            concurrently: false,
14779            target,
14780        })
14781    }
14782
14783    /// The next token as a relation / schema name, when there is one.
14784    fn take_optional_maintain_name(&mut self) -> Option<alloc::string::String> {
14785        match self.peek() {
14786            Token::Ident(_) | Token::QuotedIdent(_) => match self.advance() {
14787                Token::Ident(n) | Token::QuotedIdent(n) => Some(n),
14788                _ => None,
14789            },
14790            _ => None,
14791        }
14792    }
14793
14794    /// A parenthesised option list, absorbed.
14795    fn skip_paren_option_list(&mut self) {
14796        if !matches!(self.peek(), Token::LParen) {
14797            return;
14798        }
14799        let mut depth = 0usize;
14800        loop {
14801            match self.advance() {
14802                Token::LParen => depth += 1,
14803                Token::RParen => {
14804                    depth -= 1;
14805                    if depth == 0 {
14806                        return;
14807                    }
14808                }
14809                Token::Eof => return,
14810                _ => {}
14811            }
14812        }
14813    }
14814
14815    /// v7.39 (round 531) — the `LIKE` clause inside a CREATE TABLE
14816    /// column list.
14817    ///
14818    /// PG's option names are COMMENTS / COMPRESSION / CONSTRAINTS /
14819    /// DEFAULTS / GENERATED / IDENTITY / INDEXES / STATISTICS / STORAGE
14820    /// / ALL. The three that describe physical storage have no meaning
14821    /// here, so they parse and change nothing rather than making a
14822    /// dump that mentions them fail to load.
14823    ///
14824    /// `#[inline(never)]` because the CREATE TABLE frame sits on the
14825    /// parse chain the nesting sentinel is tuned against.
14826    #[inline(never)]
14827    fn parse_create_table_like(&mut self, at: usize) -> Result<crate::ast::LikeSpec, ParseError> {
14828        self.advance(); // LIKE
14829        let source = self.expect_ident_like()?;
14830        let mut options = crate::ast::LikeOptions::default();
14831        loop {
14832            let including = match self.peek() {
14833                Token::Ident(s) if s.eq_ignore_ascii_case("including") => true,
14834                Token::Ident(s) if s.eq_ignore_ascii_case("excluding") => false,
14835                _ => break,
14836            };
14837            self.advance();
14838            // `ALL` lexes as its own keyword, not an identifier.
14839            let opt = if matches!(self.peek(), Token::All) {
14840                self.advance();
14841                alloc::string::String::from("all")
14842            } else {
14843                self.expect_ident_like()?
14844            };
14845            let set = |o: &mut crate::ast::LikeOptions, on: bool| {
14846                o.defaults = on;
14847                o.constraints = on;
14848                o.identity = on;
14849                o.generated = on;
14850                o.indexes = on;
14851                o.comments = on;
14852            };
14853            match opt.to_ascii_lowercase().as_str() {
14854                "all" => set(&mut options, including),
14855                "defaults" => options.defaults = including,
14856                "constraints" => options.constraints = including,
14857                "identity" => options.identity = including,
14858                "generated" => options.generated = including,
14859                "indexes" => options.indexes = including,
14860                "comments" => options.comments = including,
14861                // No storage model to copy into.
14862                "storage" | "statistics" | "compression" => {}
14863                other => {
14864                    return Err(self.err(alloc::format!("unrecognized LIKE option {other:?}")));
14865                }
14866            }
14867        }
14868        Ok(crate::ast::LikeSpec {
14869            source,
14870            at,
14871            options,
14872            keep_index_names: false,
14873        })
14874    }
14875
14876    fn parse_create_table_stmt_after_create(&mut self) -> Result<Statement, ParseError> {
14877        // Caller already consumed CREATE; we're sitting on TABLE.
14878        debug_assert!(matches!(self.peek(), Token::Table));
14879        self.advance();
14880        let if_not_exists = self.consume_if_not_exists();
14881        let name = self.expect_ident_like()?;
14882        // v7.37.6-B — `CREATE TABLE c PARTITION OF parent <bounds>`
14883        // child shape has no column list; the child inherits its
14884        // columns from the parent at engine-DDL time. Detect it
14885        // before the `(` requirement below.
14886        if matches!(self.peek(), Token::Partition)
14887            && Self::tokens_match_ident_ci(self.tokens.get(self.pos + 1), "of")
14888        {
14889            self.advance(); // PARTITION
14890            self.advance(); // of
14891            let partition_of = self.parse_partition_of_tail()?;
14892            return Ok(Statement::CreateTable(CreateTableStatement {
14893                temporary: false,
14894                name,
14895                engine: None,
14896                auto_increment: None,
14897                columns: Vec::new(),
14898                like_specs: Vec::new(),
14899                inherits: Vec::new(),
14900                if_not_exists,
14901                foreign_keys: Vec::new(),
14902                table_constraints: Vec::new(),
14903                partition_by: None,
14904                partition_of: Some(partition_of),
14905            }));
14906        }
14907        // v7.38 (read01 P6.49) — CTAS: `CREATE TABLE name AS <select>`. Reuses
14908        // the materialized-view materialisation path (run the SELECT, infer the
14909        // column types, create + populate the table) but marks the node so the
14910        // executor creates a plain table without a mat-view registry entry.
14911        if matches!(self.peek(), Token::As) {
14912            self.advance();
14913            let body_stmt = self.parse_select_stmt()?;
14914            let Statement::Select(body) = body_stmt else {
14915                return Err(self.err(format!(
14916                    "CREATE TABLE {name:?} AS body must be a SELECT, got {body_stmt:?}"
14917                )));
14918            };
14919            let with_data = self.parse_optional_with_data(true)?;
14920            return Ok(Statement::CreateMaterializedView(
14921                crate::ast::CreateMaterializedViewStatement {
14922                    temporary: false,
14923                    name,
14924                    if_not_exists,
14925                    columns: Vec::new(),
14926                    body,
14927                    with_data,
14928                    as_plain_table: true,
14929                },
14930            ));
14931        }
14932        // v7.40.0 — MySQL's `CREATE TABLE b LIKE a`, which is the same
14933        // copy PostgreSQL spells `CREATE TABLE b (LIKE a INCLUDING ALL)`
14934        // written without the parentheses. It was a syntax error, so a
14935        // schema written against MySQL could not be loaded at all.
14936        //
14937        // Measured on MySQL 9.7.2: the copy takes the columns, their
14938        // defaults and the indexes, and takes neither the rows nor the
14939        // foreign keys — which is exactly `INCLUDING ALL` here, since
14940        // SPG's LIKE has never copied foreign keys.
14941        if matches!(self.peek(), Token::Like) {
14942            let at = self.pos;
14943            let spec = self.parse_create_table_like(at)?;
14944            let spec = crate::ast::LikeSpec {
14945                options: crate::ast::LikeOptions {
14946                    defaults: true,
14947                    constraints: true,
14948                    identity: true,
14949                    generated: true,
14950                    indexes: true,
14951                    comments: true,
14952                },
14953                keep_index_names: true,
14954                ..spec
14955            };
14956            return Ok(Statement::CreateTable(CreateTableStatement {
14957                temporary: false,
14958                name,
14959                engine: None,
14960                auto_increment: None,
14961                columns: Vec::new(),
14962                like_specs: alloc::vec![spec],
14963                inherits: Vec::new(),
14964                if_not_exists,
14965                foreign_keys: Vec::new(),
14966                table_constraints: Vec::new(),
14967                partition_by: None,
14968                partition_of: None,
14969            }));
14970        }
14971        if !matches!(self.peek(), Token::LParen) {
14972            return Err(self.err(format!(
14973                "expected '(' after table name, got {:?}",
14974                self.peek()
14975            )));
14976        }
14977        self.advance();
14978        let mut columns = Vec::new();
14979        let mut foreign_keys: Vec<ForeignKeyConstraint> = Vec::new();
14980        let mut table_constraints: Vec<crate::ast::TableConstraint> = Vec::new();
14981        let mut like_specs: Vec<crate::ast::LikeSpec> = Vec::new();
14982        loop {
14983            // v7.39 (round 621) — `CREATE TABLE c () INHERITS (p)`, the empty
14984            // column list. It is how a child that adds nothing of its own is
14985            // written, and this loop demanded at least one entry: `syntax
14986            // error at or near ")"`. The child takes the parent's columns,
14987            // which the INHERITS clause already arranges.
14988            if columns.is_empty() && matches!(self.peek(), Token::RParen) {
14989                self.advance();
14990                break;
14991            }
14992            // v7.6.0 / v7.9.18 — distinguish table-level constraint
14993            // clauses from column definitions. Constraints start
14994            // with `CONSTRAINT <name> …`, `FOREIGN KEY (…)`,
14995            // `PRIMARY KEY (…)`, or `UNIQUE (…)`. Anything else is
14996            // a column.
14997            if self.peek_table_level_pk_start() {
14998                table_constraints.push(self.parse_table_level_primary_key()?);
14999            } else if matches!(self.peek(), Token::Like) {
15000                // v7.39 (round 531) — `LIKE <table> [ {INCLUDING|EXCLUDING}
15001                // <opt> ]*`. The source table's shape lives in the catalog,
15002                // so this records the clause and the engine expands it.
15003                like_specs.push(self.parse_create_table_like(columns.len())?);
15004            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
15005                // v7.39 (round 210) — bare `EXCLUDE [USING m] (col WITH op, …)`.
15006                table_constraints.push(self.parse_table_level_exclude()?);
15007            } else if self.peek_table_level_unique_start() {
15008                table_constraints.push(self.parse_table_level_unique()?);
15009            } else if self.peek_table_level_check_start() {
15010                // v7.13.0 — table-level CHECK (mailrs round-5 G3).
15011                table_constraints.push(self.parse_table_level_check()?);
15012            } else if self.peek_mysql_inline_key_start() {
15013                // v7.14.0 — mysqldump emits inline `KEY name (cols)`,
15014                // `INDEX name (cols)`, `UNIQUE KEY name (cols)`,
15015                // `FULLTEXT KEY name (cols)`, `SPATIAL KEY name (cols)`
15016                // inside the column list. Skip name + paren list;
15017                // for UNIQUE KEY, register as a UC.
15018                if let Some(uc) = self.parse_mysql_inline_key()? {
15019                    table_constraints.push(uc);
15020                }
15021            } else if let Some(kind) = self.peek_named_table_constraint_kind() {
15022                // v7.22 (mailrs round-13 gap 5) — `CONSTRAINT <name>
15023                // { CHECK | UNIQUE | PRIMARY KEY }`: every pg_dump'd
15024                // CHECK is named, and the named-CONSTRAINT arm used
15025                // to accept FOREIGN KEY only. The name is accepted
15026                // and discarded — same handling as every other SPG
15027                // constraint name.
15028                self.advance(); // CONSTRAINT
15029                // v7.39 (read01 round 48) — the name is kept now: the schema
15030                // stores it, so DROP / RENAME CONSTRAINT can find it.
15031                let con_name = self.expect_ident_like()?;
15032                let mut tc = match kind {
15033                    NamedTableConstraintKind::Check => self.parse_table_level_check()?,
15034                    NamedTableConstraintKind::Unique => self.parse_table_level_unique()?,
15035                    NamedTableConstraintKind::PrimaryKey => self.parse_table_level_primary_key()?,
15036                    NamedTableConstraintKind::Exclude => self.parse_table_level_exclude()?,
15037                };
15038                match &mut tc {
15039                    crate::ast::TableConstraint::Check { name, .. }
15040                    | crate::ast::TableConstraint::Unique { name, .. }
15041                    | crate::ast::TableConstraint::PrimaryKey { name, .. }
15042                    | crate::ast::TableConstraint::Exclude { name, .. } => {
15043                        *name = Some(con_name);
15044                    }
15045                    _ => {}
15046                }
15047                table_constraints.push(tc);
15048            } else if self.peek_constraint_or_fk_start() {
15049                foreign_keys.push(self.parse_table_level_fk()?);
15050            } else {
15051                let (col, col_level_fk) = self.parse_column_def_with_fk()?;
15052                // v7.13.0 — fold inline UNIQUE / CHECK column
15053                // constraints into table-level entries so the
15054                // engine path stays uniform.
15055                if col.is_unique {
15056                    table_constraints.push(crate::ast::TableConstraint::Unique {
15057                        name: None,
15058                        columns: alloc::vec![col.name.clone()],
15059                        nulls_not_distinct: col.unique_nulls_not_distinct,
15060                        deferrable: col.constraint_deferrable,
15061                        initially_deferred: col.constraint_initially_deferred,
15062                        prefix_lengths: Vec::new(),
15063                    });
15064                }
15065                if let Some(check_expr) = col.check.clone() {
15066                    table_constraints.push(crate::ast::TableConstraint::Check {
15067                        name: None,
15068                        expr: check_expr,
15069                        not_valid: false,
15070                    });
15071                }
15072                columns.push(col);
15073                if let Some(fk) = col_level_fk {
15074                    foreign_keys.push(fk);
15075                }
15076            }
15077            match self.peek() {
15078                Token::Comma => {
15079                    self.advance();
15080                }
15081                Token::RParen => {
15082                    self.advance();
15083                    break;
15084                }
15085                other => {
15086                    return Err(
15087                        self.err(format!("expected ',' or ')' in column list, got {other:?}"))
15088                    );
15089                }
15090            }
15091        }
15092        // v7.39 (round 531) — a `LIKE` clause brings its own columns, so
15093        // `CREATE TABLE k (LIKE t)` is a complete definition even though
15094        // nothing is written between the parentheses.
15095        // v7.39 (round 621) — a table with NO columns is legal: PG creates it
15096        // and `INSERT … DEFAULT VALUES` puts a row in it. This refused, so the
15097        // empty parentheses were a parse error in their own right — quite apart
15098        // from `CREATE TABLE c () INHERITS (p)`, which needs table inheritance
15099        // SPG does not have (filed separately).
15100        let _ = &like_specs;
15101        // v7.39 (round 645) — `INHERITS (p1, p2)`, PG table inheritance.
15102        // It sits between the column list and the MySQL table options,
15103        // and it was a syntax error until this round.
15104        let mut inherits: Vec<String> = Vec::new();
15105        if matches!(self.peek(), Token::Ident(k) | Token::QuotedIdent(k)
15106            if k.eq_ignore_ascii_case("inherits"))
15107        {
15108            self.advance();
15109            if !matches!(self.peek(), Token::LParen) {
15110                return Err(self.err(alloc::format!(
15111                    "expected ( after INHERITS, got {:?}",
15112                    self.peek()
15113                )));
15114            }
15115            self.advance();
15116            loop {
15117                inherits.push(self.expect_ident_like()?);
15118                if matches!(self.peek(), Token::Comma) {
15119                    self.advance();
15120                    continue;
15121                }
15122                break;
15123            }
15124            if !matches!(self.peek(), Token::RParen) {
15125                return Err(self.err(alloc::format!(
15126                    "expected ) closing INHERITS, got {:?}",
15127                    self.peek()
15128                )));
15129            }
15130            self.advance();
15131        }
15132        // v7.14.0 — consume MySQL/MariaDB table options after the
15133        // closing `)`. mysqldump emits things like
15134        // `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
15135        // AUTO_INCREMENT=42 ROW_FORMAT=DYNAMIC COMMENT='blog posts'`.
15136        // SPG accepts all forms as no-ops (each option is
15137        // `<ident> [=] <ident-or-string>` separated by whitespace).
15138        let (engine, auto_increment) = self.consume_mysql_table_options();
15139        // v7.38 (read01 P6.55) — PG storage parameters `WITH (opt=val, …)`.
15140        // SPG has no per-table reloptions, so accept and ignore them so a
15141        // pg_dump `CREATE TABLE … WITH (fillfactor=70, …)` restores cleanly.
15142        self.consume_with_reloptions();
15143        // v7.37.6-B — declarative-partition-parent suffix
15144        // (`PARTITION BY RANGE (key_col)`) sits after the column
15145        // list + MySQL table-options. v7.37.6-B only accepts RANGE
15146        // and locks the key column at one ident; the engine then
15147        // verifies the column type is TIMESTAMPTZ.
15148        let partition_by = if matches!(self.peek(), Token::Partition) {
15149            self.advance(); // PARTITION
15150            if !self.peek_is_by() {
15151                return Err(self.err(format!(
15152                    "expected BY after PARTITION, got {:?}",
15153                    self.peek()
15154                )));
15155            }
15156            self.advance();
15157            Some(self.parse_partition_by_tail()?)
15158        } else {
15159            None
15160        };
15161        Ok(Statement::CreateTable(CreateTableStatement {
15162            temporary: false,
15163            name,
15164            engine,
15165            auto_increment,
15166            columns,
15167            like_specs,
15168            inherits,
15169            if_not_exists,
15170            foreign_keys,
15171            table_constraints,
15172            partition_by,
15173            partition_of: None,
15174        }))
15175    }
15176
15177    /// v7.37.6-B — case-insensitive ident match helper for the
15178    /// `PARTITION OF` / `MINVALUE` / `MAXVALUE` keywords. They lex
15179    /// as `Token::Ident("of"/"minvalue"/"maxvalue")` because we
15180    /// didn't burn a global keyword slot for each (see the
15181    /// `Token::Partition` doc-comment in `lexer.rs`).
15182    fn tokens_match_ident_ci(t: Option<&Token>, want: &str) -> bool {
15183        matches!(t, Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case(want))
15184    }
15185
15186    /// v7.37.6-B — after `PARTITION BY`, expect `RANGE (key_col [, ...])`.
15187    /// v7.37.16 (16.1/16.2) — extended to LIST + HASH.
15188    fn parse_partition_by_tail(&mut self) -> Result<crate::ast::PartitionBySpec, ParseError> {
15189        use crate::ast::{PartitionBySpec, PartitionKindAst};
15190        let kind = match self.peek() {
15191            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("range") => {
15192                self.advance();
15193                PartitionKindAst::Range
15194            }
15195            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("list") => {
15196                self.advance();
15197                PartitionKindAst::List
15198            }
15199            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("hash") => {
15200                self.advance();
15201                PartitionKindAst::Hash
15202            }
15203            other => {
15204                return Err(self.err(format!(
15205                    "PARTITION BY: expected RANGE / LIST / HASH, got {other:?}"
15206                )));
15207            }
15208        };
15209        if !matches!(self.peek(), Token::LParen) {
15210            return Err(self.err(format!(
15211                "expected '(' after PARTITION BY <strategy>, got {:?}",
15212                self.peek()
15213            )));
15214        }
15215        self.advance();
15216        let mut key_columns = Vec::new();
15217        loop {
15218            key_columns.push(self.expect_ident_like()?);
15219            match self.peek() {
15220                Token::Comma => {
15221                    self.advance();
15222                }
15223                Token::RParen => {
15224                    self.advance();
15225                    break;
15226                }
15227                other => {
15228                    return Err(self.err(format!(
15229                        "expected ',' or ')' in PARTITION BY key list, got {other:?}"
15230                    )));
15231                }
15232            }
15233        }
15234        if key_columns.is_empty() {
15235            return Err(self.err("PARTITION BY requires at least one key column".to_string()));
15236        }
15237        Ok(PartitionBySpec { kind, key_columns })
15238    }
15239
15240    /// v7.37.6-B — after `PARTITION OF`, expect
15241    ///   <parent> FOR VALUES FROM ( <expr> ) TO ( <expr> )
15242    /// or
15243    ///   <parent> DEFAULT
15244    fn parse_partition_of_tail(&mut self) -> Result<crate::ast::PartitionOfSpec, ParseError> {
15245        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
15246        let parent_name = self.expect_ident_like()?;
15247        // v7.37.6-B rejects an explicit column list — the child
15248        // inherits from the parent. mailrs round-7 taught us that
15249        // CREATE TABLE-side schema reconciliation hides drift, so
15250        // we surface this as a parse error rather than silently
15251        // ignoring user columns.
15252        if matches!(self.peek(), Token::LParen) {
15253            return Err(self.err(
15254                "CREATE TABLE … PARTITION OF parent: explicit column list not supported \
15255                 at v7.37.6-B; the child inherits its columns from the parent"
15256                    .to_string(),
15257            ));
15258        }
15259        let bounds = match self.peek() {
15260            Token::Default => {
15261                self.advance();
15262                PartitionOfBoundsAst::Default
15263            }
15264            Token::For => {
15265                self.advance();
15266                if !matches!(self.peek(), Token::Values) {
15267                    return Err(
15268                        self.err(format!("expected VALUES after FOR, got {:?}", self.peek()))
15269                    );
15270                }
15271                self.advance();
15272                // WITH is not a reserved Token in the lexer — it lexes
15273                // as Token::Ident("with"). Disambiguate manually.
15274                let want_with = matches!(
15275                    self.peek(),
15276                    Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15277                );
15278                if want_with {
15279                    self.advance();
15280                    if !matches!(self.peek(), Token::LParen) {
15281                        return Err(self.err(format!(
15282                            "expected '(' after FOR VALUES WITH, got {:?}",
15283                            self.peek()
15284                        )));
15285                    }
15286                    self.advance();
15287                    let (mut modulus, mut remainder): (Option<u32>, Option<u32>) = (None, None);
15288                    loop {
15289                        let key = self.expect_ident_like()?;
15290                        let n = match self.peek().clone() {
15291                            Token::Integer(v) if u32::try_from(v).is_ok() => {
15292                                self.advance();
15293                                v as u32
15294                            }
15295                            other => {
15296                                return Err(self.err(format!(
15297                                    "FOR VALUES WITH: expected unsigned integer literal, got {other:?}"
15298                                )));
15299                            }
15300                        };
15301                        match key.to_ascii_uppercase().as_str() {
15302                            "MODULUS" => modulus = Some(n),
15303                            "REMAINDER" => remainder = Some(n),
15304                            other => {
15305                                return Err(self.err(format!(
15306                                    "FOR VALUES WITH: unknown key {other:?}; \
15307                                     expected MODULUS or REMAINDER"
15308                                )));
15309                            }
15310                        }
15311                        match self.peek() {
15312                            Token::Comma => {
15313                                self.advance();
15314                            }
15315                            Token::RParen => {
15316                                self.advance();
15317                                break;
15318                            }
15319                            other => {
15320                                return Err(self.err(format!(
15321                                    "expected ',' or ')' in FOR VALUES WITH list, got {other:?}"
15322                                )));
15323                            }
15324                        }
15325                    }
15326                    let modulus = modulus
15327                        .ok_or_else(|| self.err("FOR VALUES WITH: missing MODULUS".to_string()))?;
15328                    let remainder = remainder.ok_or_else(|| {
15329                        self.err("FOR VALUES WITH: missing REMAINDER".to_string())
15330                    })?;
15331                    if modulus == 0 {
15332                        return Err(self.err("FOR VALUES WITH: MODULUS must be > 0".to_string()));
15333                    }
15334                    if remainder >= modulus {
15335                        return Err(self.err(format!(
15336                            "FOR VALUES WITH: REMAINDER ({remainder}) \
15337                             must be < MODULUS ({modulus})"
15338                        )));
15339                    }
15340                    PartitionOfBoundsAst::Hash { modulus, remainder }
15341                } else {
15342                    match self.peek() {
15343                        Token::From => {
15344                            self.advance();
15345                            let lower = Box::new(self.parse_partition_bound_expr()?);
15346                            if !matches!(self.peek(), Token::To) {
15347                                return Err(self.err(format!(
15348                                    "expected TO after FROM (...), got {:?}",
15349                                    self.peek()
15350                                )));
15351                            }
15352                            self.advance();
15353                            let upper = Box::new(self.parse_partition_bound_expr()?);
15354                            PartitionOfBoundsAst::Range { lower, upper }
15355                        }
15356                        // v7.37.16 (16.1) — FOR VALUES IN (lit [, lit, …])
15357                        Token::In => {
15358                            self.advance();
15359                            if !matches!(self.peek(), Token::LParen) {
15360                                return Err(self.err(format!(
15361                                    "expected '(' after FOR VALUES IN, got {:?}",
15362                                    self.peek()
15363                                )));
15364                            }
15365                            self.advance();
15366                            let mut values = Vec::new();
15367                            loop {
15368                                values.push(self.parse_expr(0)?);
15369                                match self.peek() {
15370                                    Token::Comma => {
15371                                        self.advance();
15372                                    }
15373                                    Token::RParen => {
15374                                        self.advance();
15375                                        break;
15376                                    }
15377                                    other => {
15378                                        return Err(self.err(format!(
15379                                        "expected ',' or ')' in FOR VALUES IN list, got {other:?}"
15380                                    )));
15381                                    }
15382                                }
15383                            }
15384                            if values.is_empty() {
15385                                return Err(self.err(
15386                                    "FOR VALUES IN requires at least one literal".to_string(),
15387                                ));
15388                            }
15389                            PartitionOfBoundsAst::List { values }
15390                        }
15391                        other => {
15392                            return Err(self.err(format!(
15393                                "expected FROM / IN / WITH after FOR VALUES, got {other:?}"
15394                            )));
15395                        }
15396                    }
15397                }
15398            }
15399            other => {
15400                return Err(self.err(format!(
15401                    "expected FOR VALUES or DEFAULT after PARTITION OF parent, got {other:?}"
15402                )));
15403            }
15404        };
15405        Ok(PartitionOfSpec {
15406            parent_name,
15407            bounds,
15408        })
15409    }
15410
15411    /// v7.37.6-B — a single `( <expr> )` bound. `MINVALUE` /
15412    /// `MAXVALUE` lex as Ident; rewrite them into FunctionCall
15413    /// markers (no-arg builtins) so the engine resolves them
15414    /// against [`spg_storage::PartitionBound::{MinValue, MaxValue}`].
15415    fn parse_partition_bound_expr(&mut self) -> Result<crate::ast::Expr, ParseError> {
15416        if !matches!(self.peek(), Token::LParen) {
15417            return Err(self.err(format!(
15418                "expected '(' before partition bound, got {:?}",
15419                self.peek()
15420            )));
15421        }
15422        self.advance();
15423        let expr = match self.peek() {
15424            Token::Ident(s) | Token::QuotedIdent(s)
15425                if s.eq_ignore_ascii_case("minvalue") || s.eq_ignore_ascii_case("maxvalue") =>
15426            {
15427                let name = s.to_ascii_uppercase();
15428                self.advance();
15429                crate::ast::Expr::FunctionCall {
15430                    name,
15431                    args: Vec::new(),
15432                }
15433            }
15434            _ => self.parse_expr(0)?,
15435        };
15436        if !matches!(self.peek(), Token::RParen) {
15437            return Err(self.err(format!(
15438                "expected ')' after partition bound, got {:?}",
15439                self.peek()
15440            )));
15441        }
15442        self.advance();
15443        Ok(expr)
15444    }
15445
15446    /// v7.14.0 — true when the next tokens look like an inline
15447    /// MySQL index declaration: KEY / INDEX / UNIQUE KEY /
15448    /// UNIQUE INDEX / FULLTEXT [KEY|INDEX] / SPATIAL [KEY|INDEX]
15449    /// — each followed by an optional name + `(...)`. Critical:
15450    /// a column NAMED `key` / `index` (PG accepts as ident) must
15451    /// NOT be mistaken for the KEY constraint shape. We disambig
15452    /// by requiring the keyword to be followed by either `(` or
15453    /// `<ident> (`.
15454    fn peek_mysql_inline_key_start(&self) -> bool {
15455        let cur = self.peek();
15456        // Shapes:
15457        //   KEY (cols)
15458        //   KEY name (cols)
15459        //   INDEX (cols)
15460        //   INDEX name (cols)
15461        //   UNIQUE KEY [name] (cols)
15462        //   UNIQUE INDEX [name] (cols)
15463        //   FULLTEXT [KEY|INDEX] [name] (cols)
15464        //   SPATIAL [KEY|INDEX] [name] (cols)
15465        let after_keyword_followed_by_paren_or_ident_paren = |skip: usize| -> bool {
15466            // tokens at skip = the position AFTER the index-form
15467            // keywords (KEY/INDEX) have been consumed.
15468            match self.tokens.get(skip) {
15469                Some(Token::LParen) => true,
15470                Some(Token::Ident(_) | Token::QuotedIdent(_)) => {
15471                    matches!(self.tokens.get(skip + 1), Some(Token::LParen))
15472                }
15473                _ => false,
15474            }
15475        };
15476        // `INDEX` lexes as Token::Index (reserved), not as
15477        // Token::Ident("index"). Both shapes count as a KEY/INDEX
15478        // start; the peek helper below handles either.
15479        let is_key_or_index_tok = |t: &Token| -> bool {
15480            matches!(t, Token::Index)
15481                || matches!(t, Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index"))
15482        };
15483        match cur {
15484            Token::Index => after_keyword_followed_by_paren_or_ident_paren(self.pos + 1),
15485            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15486                after_keyword_followed_by_paren_or_ident_paren(self.pos + 1)
15487            }
15488            Token::Ident(s)
15489                if s.eq_ignore_ascii_case("fulltext") || s.eq_ignore_ascii_case("spatial") =>
15490            {
15491                let nxt = self.tokens.get(self.pos + 1);
15492                let after_after = if nxt.is_some_and(is_key_or_index_tok) {
15493                    self.pos + 2
15494                } else {
15495                    self.pos + 1
15496                };
15497                after_keyword_followed_by_paren_or_ident_paren(after_after)
15498            }
15499            Token::Ident(s) if s.eq_ignore_ascii_case("unique") => {
15500                let nxt = self.tokens.get(self.pos + 1);
15501                if !nxt.is_some_and(is_key_or_index_tok) {
15502                    return false;
15503                }
15504                after_keyword_followed_by_paren_or_ident_paren(self.pos + 2)
15505            }
15506            _ => false,
15507        }
15508    }
15509
15510    /// v7.14.0 — parse the MySQL inline KEY/INDEX form. Returns
15511    /// Some(TableConstraint::Unique) for UNIQUE KEY (so SPG
15512    /// enforces uniqueness on INSERT). v7.15.0: plain KEY/INDEX
15513    /// returns Some(TableConstraint::Index) so the engine builds
15514    /// a real BTree index on the leading column (mysqldump
15515    /// `KEY idx_posts_author (author_id)` shape).
15516    /// FULLTEXT / SPATIAL still return None — accepted-as-no-op
15517    /// (the storage layer has no matching AM).
15518    fn parse_mysql_inline_key(
15519        &mut self,
15520    ) -> Result<Option<crate::ast::TableConstraint>, ParseError> {
15521        // Detect UNIQUE prefix.
15522        let is_unique = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unique"))
15523        {
15524            self.advance();
15525            true
15526        } else {
15527            false
15528        };
15529        // Consume FULLTEXT / SPATIAL prefix and record which one
15530        // it was. v7.17.0 Phase 2.2 — FULLTEXT routes through a
15531        // dedicated TableConstraint variant so the engine can
15532        // build a tsvector-GIN; SPATIAL still has no matching
15533        // AM, so it falls back to accept-as-no-op.
15534        let mut is_fulltext = false;
15535        let mut is_spatial = false;
15536        if let Token::Ident(s) = self.peek().clone() {
15537            if s.eq_ignore_ascii_case("fulltext") {
15538                self.advance();
15539                is_fulltext = true;
15540            } else if s.eq_ignore_ascii_case("spatial") {
15541                self.advance();
15542                is_spatial = true;
15543            }
15544        }
15545        // KEY / INDEX keyword. `INDEX` lexes as Token::Index
15546        // (reserved); accept either token shape.
15547        match self.peek() {
15548            Token::Index => {
15549                self.advance();
15550            }
15551            Token::Ident(s) if s.eq_ignore_ascii_case("key") || s.eq_ignore_ascii_case("index") => {
15552                self.advance();
15553            }
15554            other => {
15555                return Err(self.err(alloc::format!(
15556                    "expected KEY/INDEX in inline index declaration, got {other:?}"
15557                )));
15558            }
15559        }
15560        // Optional index name (an ident before the `(`).
15561        // v7.15.0 — capture the name when present so the engine
15562        // builds the secondary index under the user's chosen
15563        // name (matches mysqldump's `KEY idx_x (col)` shape).
15564        let mut idx_name: Option<String> = None;
15565        if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_))
15566            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
15567        {
15568            if let Token::Ident(s) | Token::QuotedIdent(s) = self.advance() {
15569                idx_name = Some(s);
15570            }
15571        }
15572        // Optional `USING BTREE` / `USING HASH` (MySQL).
15573        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15574            self.advance();
15575            if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15576                self.advance();
15577            }
15578        }
15579        // Required column list `(col [, col]*)`.
15580        if !matches!(self.peek(), Token::LParen) {
15581            return Err(self.err(alloc::format!(
15582                "expected '(' in inline KEY/INDEX, got {:?}",
15583                self.peek()
15584            )));
15585        }
15586        self.advance();
15587        let mut cols: Vec<String> = Vec::new();
15588        let mut prefix_lengths: Vec<Option<u32>> = Vec::new();
15589        while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
15590            self.advance();
15591            cols.push(s);
15592            // v7.40.0 — the per-column `(length)` prefix is KEPT.
15593            //
15594            // It used to be skipped, so `KEY kb (b(4))` was accepted and
15595            // the prefix forgotten: `SHOW INDEX` reported `Sub_part`
15596            // NULL and `SHOW CREATE TABLE` printed `(b)` where MySQL
15597            // 9.7.2 prints `(b(4))`. A declaration that is accepted and
15598            // then unrecorded is the worst of the three answers.
15599            let mut prefix: Option<u32> = None;
15600            if matches!(self.peek(), Token::LParen) {
15601                let mut depth = 1usize;
15602                self.advance();
15603                if let Token::Integer(n) = self.peek()
15604                    && let Ok(v) = u32::try_from(*n)
15605                {
15606                    prefix = Some(v);
15607                }
15608                while depth > 0 {
15609                    match self.peek() {
15610                        Token::LParen => depth += 1,
15611                        Token::RParen => depth -= 1,
15612                        Token::Eof => break,
15613                        _ => {}
15614                    }
15615                    self.advance();
15616                }
15617            }
15618            prefix_lengths.push(prefix);
15619            // Skip optional ASC / DESC.
15620            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asc") || s.eq_ignore_ascii_case("desc"))
15621                || matches!(self.peek(), Token::Asc | Token::Desc)
15622            {
15623                self.advance();
15624            }
15625            if matches!(self.peek(), Token::Comma) {
15626                self.advance();
15627                continue;
15628            }
15629            break;
15630        }
15631        if matches!(self.peek(), Token::RParen) {
15632            self.advance();
15633        }
15634        // Trailing options on the inline index — comment / etc.
15635        // Skip until comma or `)`.
15636        while !matches!(self.peek(), Token::Comma | Token::RParen | Token::Eof) {
15637            self.advance();
15638        }
15639        if cols.is_empty() {
15640            return Ok(None);
15641        }
15642        if is_unique {
15643            // Carry the captured idx_name on UNIQUE too so future
15644            // engine work can name the underlying BTree
15645            // accordingly; today the unique-constraint installer
15646            // synthesises the name itself, but Display round-trip
15647            // benefits from preserving it.
15648            Ok(Some(crate::ast::TableConstraint::Unique {
15649                name: idx_name,
15650                columns: cols,
15651                nulls_not_distinct: false,
15652                // MySQL inline UNIQUE KEY has no deferral vocabulary.
15653                deferrable: false,
15654                initially_deferred: false,
15655                prefix_lengths,
15656            }))
15657        } else if is_fulltext {
15658            // v7.17.0 Phase 2.2 — MySQL `FULLTEXT KEY` now
15659            // routes through `TableConstraint::FulltextIndex`;
15660            // the engine builds a tsvector-GIN over each named
15661            // column so MATCH AGAINST gets a real inverted
15662            // index instead of a silently-dropped declaration.
15663            Ok(Some(crate::ast::TableConstraint::FulltextIndex {
15664                name: idx_name,
15665                columns: cols,
15666            }))
15667        } else if is_spatial {
15668            // SPG has no native SPATIAL AM. Accept-as-no-op
15669            // (declaration is parsed, but no index is built).
15670            Ok(None)
15671        } else {
15672            // v7.15.0 — plain KEY / INDEX builds a real BTree
15673            // secondary index.
15674            Ok(Some(crate::ast::TableConstraint::Index {
15675                name: idx_name,
15676                columns: cols,
15677                prefix_lengths,
15678            }))
15679        }
15680    }
15681
15682    /// v7.14.0 — consume MySQL/MariaDB table-options tail after
15683    /// the closing `)`: ENGINE=..., DEFAULT CHARSET=...,
15684    /// COLLATE=..., AUTO_INCREMENT=N, ROW_FORMAT=..., COMMENT='...'
15685    /// (in any order, separated by whitespace).
15686    /// v7.38 (read01 P6.55) — consume and discard a PG `WITH (opt=val, …)`
15687    /// storage-parameter clause on CREATE TABLE. SPG has no per-table
15688    /// reloptions; accepting them keeps pg_dump restores working. `WITH` is a
15689    /// bare ident here, and only the parenthesised form is reloptions (so this
15690    /// never eats a `WITH DATA` / `WITH CHECK OPTION` trailer).
15691    fn consume_with_reloptions(&mut self) {
15692        let is_with = matches!(
15693            self.peek(),
15694            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("with")
15695        );
15696        if !is_with || !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
15697            return;
15698        }
15699        self.advance(); // WITH
15700        self.advance(); // (
15701        let mut depth = 1u32;
15702        while depth > 0 && !matches!(self.peek(), Token::Eof) {
15703            match self.peek() {
15704                Token::LParen => depth += 1,
15705                Token::RParen => depth -= 1,
15706                _ => {}
15707            }
15708            self.advance();
15709        }
15710    }
15711
15712    /// v7.39 — returns the `ENGINE=` name, which used to be consumed and
15713    /// dropped with everything else here. The rest of the MySQL table
15714    /// options genuinely have no meaning for SPG's storage; the engine
15715    /// name does, because MySQL REFUSES one it does not know and a dump
15716    /// with a typo in it should not quietly become a table.
15717    fn consume_mysql_table_options(&mut self) -> (Option<alloc::string::String>, Option<i64>) {
15718        let mut engine: Option<alloc::string::String> = None;
15719        let mut auto_increment: Option<i64> = None;
15720        loop {
15721            // Heuristic: a table option is an ident (or `DEFAULT`
15722            // reserved keyword) followed by `=` and an
15723            // ident / string / integer.
15724            let name_lc = match self.peek().clone() {
15725                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15726                Token::Default => alloc::string::String::from("default"),
15727                _ => break,
15728            };
15729            let known = matches!(
15730                name_lc.as_str(),
15731                "engine"
15732                    | "default"
15733                    | "charset"
15734                    | "collate"
15735                    | "auto_increment"
15736                    | "row_format"
15737                    | "comment"
15738                    | "pack_keys"
15739                    | "stats_persistent"
15740                    | "stats_auto_recalc"
15741                    | "stats_sample_pages"
15742                    | "key_block_size"
15743                    | "tablespace"
15744                    | "min_rows"
15745                    | "max_rows"
15746                    | "checksum"
15747                    | "delay_key_write"
15748                    | "insert_method"
15749                    | "data"
15750                    | "index"
15751                    | "encryption"
15752                    | "compression"
15753            );
15754            if !known {
15755                break;
15756            }
15757            self.advance(); // option name
15758            // `DEFAULT` optional prefix is followed by `CHARSET` /
15759            // `COLLATE`; consume the next ident too.
15760            if name_lc == "default" {
15761                if matches!(self.peek(), Token::Ident(_) | Token::QuotedIdent(_)) {
15762                    self.advance();
15763                }
15764            }
15765            if matches!(self.peek(), Token::Eq) {
15766                self.advance();
15767            }
15768            match self.peek().clone() {
15769                Token::Ident(v) | Token::QuotedIdent(v) | Token::String(v) => {
15770                    if name_lc == "engine" {
15771                        // v7.39.3 — as WRITTEN. MySQL 9.7.2 refuses an
15772                        // engine it does not know and names it back
15773                        // exactly: `Unknown storage engine 'NoSuchEng'`,
15774                        // measured. The lexer folds a bare identifier, so
15775                        // the message quoted a name the dump did not
15776                        // contain, which is the one thing that message is
15777                        // for. Guarded the same way the column spelling
15778                        // is: the span runs to the next token, so what
15779                        // comes back has to be the same word.
15780                        let written = self
15781                            .source_span(self.pos, self.pos)
15782                            .map(|raw| raw.trim().trim_matches('`').trim_matches('\''))
15783                            .filter(|raw| raw.eq_ignore_ascii_case(&v))
15784                            .map(alloc::string::String::from);
15785                        engine = Some(written.unwrap_or(v));
15786                    }
15787                    self.advance();
15788                }
15789                Token::Integer(v) => {
15790                    // v7.40.0 — `AUTO_INCREMENT=100` is the next value
15791                    // the table hands out, and it was consumed and
15792                    // dropped: measured on MySQL 9.7.2, the first row
15793                    // inserted into a table declared that way gets 100,
15794                    // where SPG gave it 1. `SHOW CREATE TABLE` already
15795                    // reproduces the option from the counter, so the
15796                    // dump round-tripped through a different number.
15797                    if name_lc == "auto_increment" {
15798                        auto_increment = Some(v);
15799                    }
15800                    self.advance();
15801                }
15802                _ => {}
15803            }
15804        }
15805        (engine, auto_increment)
15806    }
15807
15808    /// v7.9.18 — true when the next tokens are `PRIMARY KEY (…)`.
15809    /// PRIMARY and KEY are bare idents; we look-ahead 2 to be
15810    /// sure (otherwise a column literally named `primary` would
15811    /// be mistaken).
15812    fn peek_table_level_pk_start(&self) -> bool {
15813        let cur = self.peek();
15814        let nxt = self.tokens.get(self.pos + 1);
15815        let nxt2 = self.tokens.get(self.pos + 2);
15816        let is_primary = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("primary"));
15817        let is_key = matches!(nxt, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("key"));
15818        let is_lparen = matches!(nxt2, Some(Token::LParen));
15819        is_primary && is_key && is_lparen
15820    }
15821
15822    /// v7.9.18 — true when the next tokens are `UNIQUE (…)`.
15823    /// v7.13.0 — also matches `UNIQUE NULLS [NOT] DISTINCT (…)`
15824    /// (mailrs round-5 G10).
15825    fn peek_table_level_unique_start(&self) -> bool {
15826        let cur = self.peek();
15827        let is_unique = matches!(cur, Token::Ident(s) if s.eq_ignore_ascii_case("unique"));
15828        if !is_unique {
15829            return false;
15830        }
15831        let n1 = self.tokens.get(self.pos + 1);
15832        // Plain `UNIQUE (…)`.
15833        if matches!(n1, Some(Token::LParen)) {
15834            return true;
15835        }
15836        // `UNIQUE NULLS [NOT] DISTINCT (…)`.
15837        let is_nulls = matches!(n1, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("nulls"));
15838        if !is_nulls {
15839            return false;
15840        }
15841        let n2 = self.tokens.get(self.pos + 2);
15842        let n3 = self.tokens.get(self.pos + 3);
15843        let n4 = self.tokens.get(self.pos + 4);
15844        // `UNIQUE NULLS DISTINCT (…)` — 4 tokens before `(`.
15845        if matches!(n2, Some(Token::Distinct)) && matches!(n3, Some(Token::LParen)) {
15846            return true;
15847        }
15848        // `UNIQUE NULLS NOT DISTINCT (…)` — 5 tokens before `(`.
15849        if matches!(n2, Some(Token::Not))
15850            && matches!(n3, Some(Token::Distinct))
15851            && matches!(n4, Some(Token::LParen))
15852        {
15853            return true;
15854        }
15855        false
15856    }
15857
15858    fn parse_table_level_primary_key(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15859        self.advance(); // PRIMARY
15860        self.advance(); // KEY
15861        let columns = self.parse_paren_ident_list("PRIMARY KEY")?;
15862        // v7.39 (round 711) — the trailer's values are CARRIED now; round
15863        // 621 consumed and dropped them (the storing half of F08).
15864        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15865        Ok(crate::ast::TableConstraint::PrimaryKey {
15866            name: None,
15867            columns,
15868            deferrable,
15869            initially_deferred,
15870        })
15871    }
15872
15873    fn parse_table_level_unique(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15874        self.advance(); // UNIQUE
15875        // v7.13.0 — optional `NULLS NOT DISTINCT` modifier
15876        // (mailrs round-5 G10, PG 15+ surface). Default behaviour
15877        // is `NULLS DISTINCT` per the SQL standard.
15878        let mut nulls_not_distinct = false;
15879        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
15880            let n1 = self.tokens.get(self.pos + 1);
15881            let n2 = self.tokens.get(self.pos + 2);
15882            let is_not = matches!(n1, Some(Token::Not));
15883            let is_distinct = matches!(n2, Some(Token::Distinct));
15884            if is_not && is_distinct {
15885                self.advance(); // NULLS
15886                self.advance(); // NOT
15887                self.advance(); // DISTINCT
15888                nulls_not_distinct = true;
15889            } else if matches!(n1, Some(Token::Distinct)) {
15890                self.advance(); // NULLS
15891                self.advance(); // DISTINCT
15892            }
15893        }
15894        let columns = self.parse_paren_ident_list("UNIQUE")?;
15895        let (deferrable, initially_deferred) = self.consume_deferrable_clauses_timed()?;
15896        Ok(crate::ast::TableConstraint::Unique {
15897            name: None,
15898            columns,
15899            nulls_not_distinct,
15900            deferrable,
15901            initially_deferred,
15902            prefix_lengths: Vec::new(),
15903        })
15904    }
15905
15906    /// v7.13.0 — table-level `CHECK (<expr>)` constraint
15907    /// (mailrs round-5 G3). Consumes `CHECK` then a parenthesised
15908    /// expression.
15909    /// v7.39 (round 210) — `EXCLUDE [USING <method>] ( <col> WITH <op>
15910    /// [, <col> WITH <op>]* ) [WHERE (...)]`. The operator is read as a
15911    /// standalone token spelling (`&&`, `=`, `@>`, `<@`, `&<`, `&>`).
15912    /// v7.39 (round 652) — the optional `NOT VALID` suffix on a
15913    /// constraint added by ALTER TABLE. `NOT` alone is not enough to
15914    /// commit: `NOT` starts no other suffix here, but reading both
15915    /// tokens before advancing keeps the caller's error message intact
15916    /// if someone writes `NOT NULL` by mistake.
15917    fn parse_not_valid_suffix(&mut self) -> bool {
15918        if !matches!(self.peek(), Token::Not) {
15919            return false;
15920        }
15921        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("valid"))
15922        {
15923            return false;
15924        }
15925        self.advance();
15926        self.advance();
15927        true
15928    }
15929
15930    fn parse_table_level_exclude(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
15931        self.advance(); // EXCLUDE
15932        // Optional `USING <method>`.
15933        let mut method = None;
15934        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
15935            self.advance();
15936            method = Some(match self.advance() {
15937                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
15938                other => {
15939                    return Err(self.err(alloc::format!(
15940                        "expected index method after USING, got {other:?}"
15941                    )));
15942                }
15943            });
15944        }
15945        if !matches!(self.peek(), Token::LParen) {
15946            return Err(self.err(alloc::format!(
15947                "expected '(' after EXCLUDE, got {:?}",
15948                self.peek()
15949            )));
15950        }
15951        self.advance();
15952        let mut elements: Vec<(String, String)> = Vec::new();
15953        loop {
15954            let col = match self.advance() {
15955                Token::Ident(s) | Token::QuotedIdent(s) => s,
15956                other => {
15957                    return Err(self.err(alloc::format!(
15958                        "expected column name in EXCLUDE, got {other:?}"
15959                    )));
15960                }
15961            };
15962            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
15963                return Err(self.err(alloc::format!(
15964                    "expected WITH after EXCLUDE column, got {:?}",
15965                    self.peek()
15966                )));
15967            }
15968            self.advance();
15969            let op = match self.advance() {
15970                Token::InetOverlap => String::from("&&"),
15971                Token::Intersects => String::from("?#"),
15972                Token::IsBelow => String::from("<^"),
15973                Token::IsAbove => String::from(">^"),
15974                Token::PatternLt => String::from("~<~"),
15975                Token::PatternLtEq => String::from("~<=~"),
15976                Token::PatternGt => String::from("~>~"),
15977                Token::PatternGtEq => String::from("~>=~"),
15978                Token::TsMatchOld => String::from("@@@"),
15979                Token::Eq => String::from("="),
15980                Token::JsonContains => String::from("@>"),
15981                Token::JsonContainedBy => String::from("<@"),
15982                Token::OverLeft => String::from("&<"),
15983                Token::OverRight => String::from("&>"),
15984                other => {
15985                    return Err(self.err(alloc::format!(
15986                        "unsupported EXCLUDE operator {other:?} (SPG supports &&, =, @>, <@, &<, &>)"
15987                    )));
15988                }
15989            };
15990            elements.push((col, op));
15991            if matches!(self.peek(), Token::Comma) {
15992                self.advance();
15993                continue;
15994            }
15995            break;
15996        }
15997        if !matches!(self.peek(), Token::RParen) {
15998            return Err(self.err(alloc::format!(
15999                "expected ')' to close EXCLUDE, got {:?}",
16000                self.peek()
16001            )));
16002        }
16003        self.advance();
16004        Ok(crate::ast::TableConstraint::Exclude {
16005            name: None,
16006            method,
16007            elements,
16008        })
16009    }
16010
16011    fn parse_table_level_check(&mut self) -> Result<crate::ast::TableConstraint, ParseError> {
16012        self.advance(); // CHECK
16013        if !matches!(self.peek(), Token::LParen) {
16014            return Err(self.err(alloc::format!(
16015                "expected '(' after CHECK, got {:?}",
16016                self.peek()
16017            )));
16018        }
16019        self.advance();
16020        let expr = self.parse_expr(0)?;
16021        if !matches!(self.peek(), Token::RParen) {
16022            return Err(self.err(alloc::format!(
16023                "expected ')' to close CHECK predicate, got {:?}",
16024                self.peek()
16025            )));
16026        }
16027        self.advance();
16028        // A CHECK written inside CREATE TABLE cannot be NOT VALID: there
16029        // are no existing rows for PG to skip, so it rejects the suffix.
16030        Ok(crate::ast::TableConstraint::Check {
16031            name: None,
16032            expr,
16033            not_valid: false,
16034        })
16035    }
16036
16037    /// v7.13.0 — `true` when the next token is `CHECK` (a bare ident).
16038    fn peek_table_level_check_start(&self) -> bool {
16039        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("check"))
16040    }
16041
16042    /// v7.22 (round-13 gap 5) — `Some(kind)` when the next tokens are
16043    /// `CONSTRAINT <name> { CHECK | UNIQUE | PRIMARY }`. FOREIGN stays
16044    /// on the dedicated FK path (`parse_table_level_fk` consumes its
16045    /// own CONSTRAINT prefix).
16046    fn peek_named_table_constraint_kind(&self) -> Option<NamedTableConstraintKind> {
16047        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16048            return None;
16049        }
16050        // tokens[pos+1] is the constraint name (any ident-like);
16051        // tokens[pos+2] is the kind keyword.
16052        match self.tokens.get(self.pos + 2) {
16053            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("check") => {
16054                Some(NamedTableConstraintKind::Check)
16055            }
16056            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("unique") => {
16057                Some(NamedTableConstraintKind::Unique)
16058            }
16059            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("primary") => {
16060                Some(NamedTableConstraintKind::PrimaryKey)
16061            }
16062            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exclude") => {
16063                Some(NamedTableConstraintKind::Exclude)
16064            }
16065            _ => None,
16066        }
16067    }
16068
16069    fn parse_paren_ident_list(&mut self, ctx: &str) -> Result<Vec<String>, ParseError> {
16070        if !matches!(self.peek(), Token::LParen) {
16071            return Err(self.err(alloc::format!(
16072                "expected '(' after {ctx}, got {:?}",
16073                self.peek()
16074            )));
16075        }
16076        self.advance();
16077        let mut out = Vec::new();
16078        loop {
16079            out.push(self.expect_ident_like()?);
16080            match self.peek() {
16081                Token::Comma => {
16082                    self.advance();
16083                }
16084                Token::RParen => {
16085                    self.advance();
16086                    break;
16087                }
16088                other => {
16089                    return Err(self.err(alloc::format!(
16090                        "expected ',' or ')' in {ctx} list, got {other:?}"
16091                    )));
16092                }
16093            }
16094        }
16095        if out.is_empty() {
16096            return Err(self.err(alloc::format!("{ctx} requires at least one column")));
16097        }
16098        Ok(out)
16099    }
16100
16101    /// v7.6.0 — true when the next tokens are `CONSTRAINT <name>
16102    /// FOREIGN KEY` or bare `FOREIGN KEY`. Both introduce a
16103    /// table-level FK; a column def never starts with either keyword
16104    /// (column names are not in this reserved set).
16105    fn peek_constraint_or_fk_start(&self) -> bool {
16106        let is_constraint_kw = matches!(
16107            self.peek(),
16108            Token::Ident(s) if s.eq_ignore_ascii_case("constraint")
16109        );
16110        let is_foreign_kw = matches!(
16111            self.peek(),
16112            Token::Ident(s) if s.eq_ignore_ascii_case("foreign")
16113        );
16114        is_constraint_kw || is_foreign_kw
16115    }
16116
16117    /// v7.6.0 — parse a table-level FK clause:
16118    /// `[CONSTRAINT <name>] FOREIGN KEY (<col>[,<col>]*) REFERENCES
16119    /// <tbl> [(<pcol>[,<pcol>]*)] [ON DELETE <action>] [ON UPDATE <action>]`.
16120    fn parse_table_level_fk(&mut self) -> Result<ForeignKeyConstraint, ParseError> {
16121        let mut name: Option<String> = None;
16122        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
16123            self.advance();
16124            name = Some(self.expect_ident_like()?);
16125        }
16126        // `FOREIGN`
16127        match self.advance() {
16128            Token::Ident(s) if s.eq_ignore_ascii_case("foreign") => {}
16129            other => return Err(self.err(format!("expected FOREIGN, got {other:?}"))),
16130        }
16131        // `KEY`
16132        match self.advance() {
16133            Token::Ident(s) if s.eq_ignore_ascii_case("key") => {}
16134            other => return Err(self.err(format!("expected KEY after FOREIGN, got {other:?}"))),
16135        }
16136        // `(col, col, ...)`
16137        if !matches!(self.peek(), Token::LParen) {
16138            return Err(self.err(format!(
16139                "expected '(' after FOREIGN KEY, got {:?}",
16140                self.peek()
16141            )));
16142        }
16143        self.advance();
16144        let mut columns = Vec::new();
16145        loop {
16146            columns.push(self.expect_ident_like()?);
16147            match self.peek() {
16148                Token::Comma => {
16149                    self.advance();
16150                }
16151                Token::RParen => {
16152                    self.advance();
16153                    break;
16154                }
16155                other => {
16156                    return Err(self.err(format!(
16157                        "expected ',' or ')' in FK column list, got {other:?}"
16158                    )));
16159                }
16160            }
16161        }
16162        if columns.is_empty() {
16163            return Err(self.err("FOREIGN KEY requires at least one column".into()));
16164        }
16165        let (
16166            parent_table,
16167            parent_columns,
16168            on_delete,
16169            on_update,
16170            match_type,
16171            deferrable,
16172            initially_deferred,
16173        ) = self.parse_references_tail(columns.len())?;
16174        Ok(ForeignKeyConstraint {
16175            name,
16176            columns,
16177            parent_table,
16178            parent_columns,
16179            on_delete,
16180            on_update,
16181            match_type,
16182            deferrable,
16183            initially_deferred,
16184        })
16185    }
16186
16187    /// v7.6.0 — parse the tail `REFERENCES <tbl> [(<pcol>...)] [ON
16188    /// DELETE <action>] [ON UPDATE <action>]`. `expected_arity` is
16189    /// the local column count, used to default the parent column
16190    /// list when omitted (SQL spec: parent's PK is implied).
16191    fn parse_references_tail(
16192        &mut self,
16193        expected_arity: usize,
16194    ) -> Result<
16195        (
16196            String,
16197            Vec<String>,
16198            FkAction,
16199            FkAction,
16200            crate::ast::MatchType,
16201            // v7.39 (round 288) — deferrable, initially_deferred.
16202            bool,
16203            bool,
16204        ),
16205        ParseError,
16206    > {
16207        match self.advance() {
16208            Token::Ident(s) if s.eq_ignore_ascii_case("references") => {}
16209            other => return Err(self.err(format!("expected REFERENCES, got {other:?}"))),
16210        }
16211        let parent_table = self.expect_ident_like()?;
16212        let mut parent_columns: Vec<String> = Vec::new();
16213        if matches!(self.peek(), Token::LParen) {
16214            self.advance();
16215            loop {
16216                parent_columns.push(self.expect_ident_like()?);
16217                match self.peek() {
16218                    Token::Comma => {
16219                        self.advance();
16220                    }
16221                    Token::RParen => {
16222                        self.advance();
16223                        break;
16224                    }
16225                    other => {
16226                        return Err(self.err(format!(
16227                            "expected ',' or ')' in REFERENCES column list, got {other:?}"
16228                        )));
16229                    }
16230                }
16231            }
16232        }
16233        if !parent_columns.is_empty() && parent_columns.len() != expected_arity {
16234            return Err(self.err(format!(
16235                "FK arity mismatch: {} local column(s) vs {} parent column(s)",
16236                expected_arity,
16237                parent_columns.len()
16238            )));
16239        }
16240        // Optional `MATCH {SIMPLE | FULL | PARTIAL}`. PG's grammar puts
16241        // it between the referenced column list and the ON / DEFERRABLE
16242        // trailers. SPG implements MATCH SIMPLE semantics (the FK check
16243        // is skipped when any referencing column is NULL), so SIMPLE —
16244        // the default, and the only spelling pg_dump emits — is accepted
16245        // as a no-op. MATCH FULL / MATCH PARTIAL need the per-FK
16246        // mixed-NULL rule, which is not wired yet; reject them honestly
16247        // rather than silently applying SIMPLE (PG itself errors on
16248        // MATCH PARTIAL as "not yet implemented").
16249        let mut match_type = crate::ast::MatchType::Simple;
16250        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("match")) {
16251            self.advance();
16252            // `FULL` is a reserved keyword token (FULL OUTER JOIN);
16253            // SIMPLE / PARTIAL arrive as bare identifiers.
16254            let kind = match self.advance() {
16255                Token::Full => "FULL".to_string(),
16256                Token::Ident(s) => s.to_uppercase(),
16257                other => {
16258                    return Err(self.err(format!(
16259                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {other:?}"
16260                    )));
16261                }
16262            };
16263            match kind.as_str() {
16264                "SIMPLE" => {} // Default — match_type stays Simple.
16265                // v7.38 (read01, T29) — MATCH FULL: the check is skipped only
16266                // when ALL referencing columns are NULL; a mixed-NULL key errors.
16267                "FULL" => match_type = crate::ast::MatchType::Full,
16268                "PARTIAL" => {
16269                    return Err(self.err("MATCH PARTIAL not yet implemented".to_string()));
16270                }
16271                _ => {
16272                    return Err(self.err(format!(
16273                        "expected FULL, PARTIAL or SIMPLE after MATCH, got {kind}"
16274                    )));
16275                }
16276            }
16277        }
16278        // v7.6.7 / v7.17.0 Phase 3.1 — interleave `[NOT] DEFERRABLE
16279        // [INITIALLY {DEFERRED | IMMEDIATE}]` and `ON DELETE
16280        // <action>` / `ON UPDATE <action>` in either order. PG /
16281        // pg_dump emits the timing clause AFTER the ON clauses
16282        // (`ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED`),
16283        // but the SQL spec allows either order. We loop over
16284        // every possible trailer and dispatch on the next token,
16285        // stopping when nothing matches. Phase 3.1 changes the
16286        // bare DEFERRABLE form from hard-error to accept-as-
16287        // immediate; SPG is single-writer with no deferred-
16288        // constraint window so the runtime semantics are always
16289        // immediate even when INITIALLY DEFERRED is requested.
16290        // PG's default referential action (no ON DELETE / ON UPDATE
16291        // clause) is NO ACTION, not RESTRICT — the two enforce
16292        // identically in SPG (single-writer, no deferred window; see the
16293        // shared match arm in constraints.rs) but information_schema.
16294        // referential_constraints must report NO ACTION to match PG.
16295        let mut on_delete = FkAction::NoAction;
16296        let mut on_update = FkAction::NoAction;
16297        let mut seen_on_delete = false;
16298        let mut seen_on_update = false;
16299        let mut deferrable = false;
16300        let mut initially_deferred = false;
16301        loop {
16302            // DEFERRABLE / NOT DEFERRABLE / INITIALLY shapes.
16303            let before = self.pos;
16304            let (d, idef) = self.consume_deferrable_clauses_timed()?;
16305            if self.pos != before {
16306                deferrable = d;
16307                initially_deferred = idef;
16308                continue;
16309            }
16310            // ON DELETE / ON UPDATE.
16311            if !matches!(self.peek(), Token::On) {
16312                break;
16313            }
16314            self.advance();
16315            let which = self.advance();
16316            let action = self.parse_fk_action()?;
16317            match which {
16318                Token::Ident(ref s) if s.eq_ignore_ascii_case("delete") => {
16319                    if seen_on_delete {
16320                        return Err(self.err("ON DELETE specified twice".into()));
16321                    }
16322                    seen_on_delete = true;
16323                    on_delete = action;
16324                }
16325                Token::Ident(ref s) if s.eq_ignore_ascii_case("update") => {
16326                    if seen_on_update {
16327                        return Err(self.err("ON UPDATE specified twice".into()));
16328                    }
16329                    seen_on_update = true;
16330                    on_update = action;
16331                }
16332                other => {
16333                    return Err(
16334                        self.err(format!("expected DELETE or UPDATE after ON, got {other:?}"))
16335                    );
16336                }
16337            }
16338        }
16339        Ok((
16340            parent_table,
16341            parent_columns,
16342            on_delete,
16343            on_update,
16344            match_type,
16345            deferrable,
16346            initially_deferred,
16347        ))
16348    }
16349
16350    /// v7.6.0 — parse `CASCADE | RESTRICT | SET NULL | SET DEFAULT |
16351    /// NO ACTION`.
16352    fn parse_fk_action(&mut self) -> Result<FkAction, ParseError> {
16353        match self.advance() {
16354            Token::Ident(s) if s.eq_ignore_ascii_case("cascade") => Ok(FkAction::Cascade),
16355            Token::Ident(s) if s.eq_ignore_ascii_case("restrict") => Ok(FkAction::Restrict),
16356            Token::Ident(s) if s.eq_ignore_ascii_case("set") => match self.advance() {
16357                Token::Null => Ok(FkAction::SetNull),
16358                Token::Default => Ok(FkAction::SetDefault),
16359                other => Err(self.err(format!(
16360                    "expected NULL or DEFAULT after SET in FK action, got {other:?}"
16361                ))),
16362            },
16363            Token::Ident(s) if s.eq_ignore_ascii_case("no") => match self.advance() {
16364                Token::Ident(s) if s.eq_ignore_ascii_case("action") => Ok(FkAction::NoAction),
16365                other => Err(self.err(format!(
16366                    "expected ACTION after NO in FK action, got {other:?}"
16367                ))),
16368            },
16369            other => Err(self.err(format!(
16370                "expected CASCADE | RESTRICT | SET NULL | SET DEFAULT | NO ACTION, got {other:?}"
16371            ))),
16372        }
16373    }
16374
16375    /// Recognise the optional `IF NOT EXISTS` prefix shared by `CREATE
16376    /// TABLE` and `CREATE INDEX`. Returns `true` if consumed.
16377    fn consume_if_not_exists(&mut self) -> bool {
16378        // `IF` arrives as a bare Ident (we don't reserve it because it
16379        // also appears mid-expression in PG, though we don't support
16380        // those forms yet).
16381        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16382        if !looks_like_if {
16383            return false;
16384        }
16385        // Peek one ahead before committing: only consume IF when it's
16386        // actually `IF NOT EXISTS`.
16387        if !matches!(self.tokens.get(self.pos + 1), Some(Token::Not)) {
16388            return false;
16389        }
16390        if !matches!(
16391            self.tokens.get(self.pos + 2),
16392            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16393        ) {
16394            return false;
16395        }
16396        self.advance(); // IF
16397        self.advance(); // NOT
16398        self.advance(); // EXISTS
16399        true
16400    }
16401
16402    /// v7.12.4 — `IF EXISTS` modifier for DROP statements.
16403    /// Consumes IF EXISTS as a pair; returns false otherwise
16404    /// without consuming any tokens.
16405    /// v7.39 (RLS) — consume the `ROW LEVEL SECURITY` keyword triple after
16406    /// ENABLE/DISABLE/FORCE/NO FORCE.
16407    fn expect_row_level_security(&mut self) -> Result<(), ParseError> {
16408        for kw in ["row", "level", "security"] {
16409            if !matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case(kw))
16410            {
16411                return Err(self.err(alloc::format!(
16412                    "expected {} in ROW LEVEL SECURITY, got {:?}",
16413                    kw.to_ascii_uppercase(),
16414                    self.peek()
16415                )));
16416            }
16417            self.advance();
16418        }
16419        Ok(())
16420    }
16421
16422    fn consume_if_exists(&mut self) -> bool {
16423        let looks_like_if = matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("if"));
16424        if !looks_like_if {
16425            return false;
16426        }
16427        if !matches!(
16428            self.tokens.get(self.pos + 1),
16429            Some(Token::Ident(s)) if s.eq_ignore_ascii_case("exists")
16430        ) {
16431            return false;
16432        }
16433        self.advance(); // IF
16434        self.advance(); // EXISTS
16435        true
16436    }
16437
16438    /// v7.9.14 — consume `ASC | DESC | NULLS FIRST | NULLS LAST`
16439    /// qualifiers after an index column ref. ASC / DESC are
16440    /// reserved tokens; NULLS / FIRST / LAST are bare idents.
16441    /// We accept and discard them since single-column BTree
16442    /// stores rows in natural key order today.
16443    /// v7.24 (round-16 A) — `NULLS FIRST` / `NULLS LAST` after an
16444    /// ORDER BY key. Returns None when absent.
16445    fn parse_optional_nulls_placement(&mut self) -> Result<Option<bool>, ParseError> {
16446        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16447            return Ok(None);
16448        }
16449        self.advance();
16450        match self.advance() {
16451            Token::Ident(s) if s.eq_ignore_ascii_case("first") => Ok(Some(true)),
16452            Token::Ident(s) if s.eq_ignore_ascii_case("last") => Ok(Some(false)),
16453            other => Err(self.err(alloc::format!(
16454                "expected FIRST or LAST after NULLS, got {other:?}"
16455            ))),
16456        }
16457    }
16458
16459    /// v7.39 (round 537) — the per-column ordering clause, REPORTED now
16460    /// rather than discarded.
16461    ///
16462    /// SPG's index does not scan in a direction — column ordering is
16463    /// intrinsic to the storage — but `pg_indexes.indexdef` is a
16464    /// reproduction of the DDL, and dropping the clause meant
16465    /// `CREATE INDEX i ON t (a DESC NULLS LAST)` read back as `(a)`. A
16466    /// dump lost it, and a schema diff saw drift on every run.
16467    fn consume_optional_index_column_qualifiers(&mut self) -> crate::ast::IndexColumnOrder {
16468        let mut order = crate::ast::IndexColumnOrder::default();
16469        loop {
16470            match self.peek() {
16471                Token::Asc => {
16472                    self.advance();
16473                }
16474                Token::Desc => {
16475                    order.descending = true;
16476                    self.advance();
16477                }
16478                Token::Ident(s) if s.eq_ignore_ascii_case("nulls") => {
16479                    let look = self.tokens.get(self.pos + 1);
16480                    if matches!(
16481                        look,
16482                        Some(Token::Ident(k)) if k.eq_ignore_ascii_case("first")
16483                            || k.eq_ignore_ascii_case("last")
16484                    ) {
16485                        self.advance();
16486                        order.nulls_first = Some(matches!(
16487                            self.advance(),
16488                            Token::Ident(k) if k.eq_ignore_ascii_case("first")
16489                        ));
16490                    } else {
16491                        break;
16492                    }
16493                }
16494                _ => break,
16495            }
16496        }
16497        order
16498    }
16499
16500    fn parse_create_index_stmt_after_create(
16501        &mut self,
16502        is_unique: bool,
16503    ) -> Result<Statement, ParseError> {
16504        // Caller consumed CREATE (and the optional UNIQUE); we're on INDEX.
16505        debug_assert!(matches!(self.peek(), Token::Index));
16506        self.advance();
16507        // v7.37.17 (17.6 partial) — CONCURRENTLY noise word (PG 8.2+).
16508        // SPG's CREATE INDEX is synchronous end-to-end today (real
16509        // CONCURRENTLY variant with restartable scans queues with
16510        // v7.39 indexes epic), so the modifier has no runtime effect
16511        // — same accept-and-no-op shape as v7.37.16.5 DETACH
16512        // PARTITION CONCURRENTLY and v7.37.19.8 REFRESH MATERIALIZED
16513        // VIEW CONCURRENTLY.
16514        let mut concurrently = false;
16515        if matches!(
16516            self.peek(),
16517            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("concurrently")
16518        ) {
16519            self.advance();
16520            concurrently = true;
16521        }
16522        let if_not_exists = self.consume_if_not_exists();
16523        // v7.39 (read01 round 93) — the index name is optional (PG since
16524        // forever): `CREATE INDEX ON t (a)` lets the server pick a name.
16525        // When the token after `[IF NOT EXISTS]` is already `ON`, no name
16526        // was given; leave it empty and the engine derives a PG-style
16527        // `<table>_<cols>_idx` name at CREATE time (with collision counter).
16528        let name = if matches!(self.peek(), Token::On) {
16529            String::new()
16530        } else {
16531            self.expect_ident_like()?
16532        };
16533        if !matches!(self.peek(), Token::On) {
16534            return Err(self.err(format!(
16535                "expected ON after CREATE INDEX <name>, got {:?}",
16536                self.peek()
16537            )));
16538        }
16539        self.advance();
16540        let table = self.expect_ident_like()?;
16541        // Optional `USING <method>` — only recognised method in v2.0 is
16542        // `hnsw` (a single-layer NSW graph for kNN). `USING` is the bare
16543        // ident `using` (we don't promote it to a reserved keyword
16544        // because it isn't reserved anywhere else in our SQL surface).
16545        let mut method_name: Option<String> = None;
16546        let method = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
16547            self.advance();
16548            let m = self.expect_ident_like()?;
16549            method_name = Some(m.to_ascii_lowercase());
16550            match m.to_ascii_lowercase().as_str() {
16551                "hnsw" => IndexMethod::Hnsw,
16552                "btree" => IndexMethod::BTree,
16553                "brin" => IndexMethod::Brin,
16554                // v7.12.3 — real GIN inverted index over `tsvector`.
16555                // v7.9.26b's `USING gin` → BTree silent fallback is
16556                // gone; the engine validates that the indexed column
16557                // is `tsvector` at CREATE INDEX time.
16558                "gin" => IndexMethod::Gin,
16559                // v7.9.26b — PG `pg_dump` emits `USING gist` /
16560                // `USING spgist` / `USING hash` for their built-in
16561                // AMs that SPG doesn't have a matching
16562                // implementation for; degrade to BTree on the
16563                // leading column so the schema loads + the index
16564                // catalogue stays consistent. Operator pays the
16565                // planner cost only for the queries that would have
16566                // used the specialised AM.
16567                "gist" | "spgist" | "hash" => IndexMethod::BTree,
16568                // v7.11.3 — pgvector ships both `ivfflat` and
16569                // `hnsw`. Customers shouldn't have to choose
16570                // their on-disk index method based on what SPG
16571                // implements; accept `ivfflat` as a synonym for
16572                // `hnsw` so PG schemas using either method drop
16573                // in. The vector distance op (`<->` / `<#>` /
16574                // `<=>`) at query time still picks the metric.
16575                "ivfflat" => IndexMethod::Hnsw,
16576                other => {
16577                    return Err(self.err(alloc::format!(
16578                        "unknown index method {other:?}; supported: hnsw, btree, brin, gin (gist/spgist/hash accepted as BTree fallback)"
16579                    )));
16580                }
16581            }
16582        } else {
16583            IndexMethod::BTree
16584        };
16585        if !matches!(self.peek(), Token::LParen) {
16586            return Err(self.err(format!(
16587                "expected '(' before indexed column, got {:?}",
16588                self.peek()
16589            )));
16590        }
16591        self.advance();
16592        // v6.8.2 — accept either a bare column ident (legacy) or
16593        // an expression `fn(col, …)` for expression indexes.
16594        // Distinguish by peeking the token *after* the current
16595        // ident: `ident )` is the legacy column-only path;
16596        // anything else triggers the Pratt expression parser.
16597        // (`advance()` uses `mem::replace` to nil out the current
16598        // slot, so we can't save+rewind cleanly — peek-ahead via
16599        // direct index avoids the mutation.)
16600        let mut opclass: Option<String> = None;
16601        let mut key_collation: Option<String> = None;
16602        let (column, expression): (String, Option<Expr>) = match self.peek().clone() {
16603            // Single column with `)` immediately after — fast path.
16604            // v7.9.29 — also: bare column followed by `,` (the
16605            // multi-column form `(a, b, c)`). Without this branch
16606            // the leading ident gets pulled into `parse_expr`
16607            // which then sets `expression = Some(Column(a))` and
16608            // breaks Display round-trip on the multi-column shape.
16609            Token::Ident(s) | Token::QuotedIdent(s)
16610                if matches!(
16611                    self.tokens.get(self.pos + 1),
16612                    Some(Token::RParen | Token::Comma)
16613                ) =>
16614            {
16615                self.advance();
16616                (s, None)
16617            }
16618            // v7.9.22 — single column followed by a pgvector
16619            // opclass ident: `(col vector_cosine_ops)`. mailrs G5.
16620            // v7.15.0 — capture the opclass instead of discarding
16621            // it so the engine can dispatch (e.g. `gin_trgm_ops`
16622            // → real trigram-shingle GIN over a TEXT column).
16623            // Vector/HNSW opclasses still take their distance
16624            // metric from the query operator (`<->` / `<#>` /
16625            // `<=>`), so for those callers the opclass stays
16626            // informational.
16627            // v7.22 (mailrs round-13 gap 7) — pg_dump qualifies the
16628            // opclass: `(embedding public.vector_cosine_ops)`. Strip
16629            // the schema and dispatch on the bare opclass, the same
16630            // treatment table/type names get.
16631            Token::Ident(s) | Token::QuotedIdent(s)
16632                if matches!(
16633                    self.tokens.get(self.pos + 1),
16634                    Some(Token::Ident(_) | Token::QuotedIdent(_))
16635                ) && matches!(self.tokens.get(self.pos + 2), Some(Token::Dot))
16636                    && matches!(
16637                        self.tokens.get(self.pos + 3),
16638                        Some(Token::Ident(op) | Token::QuotedIdent(op))
16639                            if is_vector_opclass_name(op)
16640                    ) =>
16641            {
16642                self.advance(); // column name
16643                self.advance(); // schema qualifier
16644                self.advance(); // dot
16645                let op_tok = self.advance();
16646                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16647                    opclass = Some(op.to_ascii_lowercase());
16648                }
16649                (s, None)
16650            }
16651            // r1038 — an operator class is recognised by its POSITION, not
16652            // by a list of names. It used to be `is_vector_opclass_name`,
16653            // so `USING gin (doc jsonb_path_ops)` — ordinary PG, and what
16654            // sentori's migration wrote — was a syntax error while
16655            // `USING gin (doc)` parsed. Anything sitting between a column
16656            // name and a `,` `)` ASC DESC NULLS COLLATE is an opclass;
16657            // two bare identifiers in a row are not valid there otherwise.
16658            Token::Ident(s) | Token::QuotedIdent(s)
16659                if matches!(
16660                    self.tokens.get(self.pos + 1),
16661                    Some(Token::Ident(op) | Token::QuotedIdent(op))
16662                        if is_vector_opclass_name(op) || Self::opclass_position_follows(
16663                            self.tokens.get(self.pos + 2)
16664                        )
16665                ) =>
16666            {
16667                self.advance(); // column name
16668                // Capture the opclass token, lower-cased for
16669                // case-insensitive engine dispatch.
16670                let op_tok = self.advance();
16671                if let Token::Ident(op) | Token::QuotedIdent(op) = op_tok {
16672                    opclass = Some(op.to_ascii_lowercase());
16673                }
16674                (s, None)
16675            }
16676            Token::Ident(_) | Token::QuotedIdent(_) => {
16677                // v7.39 (round 538) — an explicit COLLATE on the key,
16678                // read by LOOKAHEAD because `parse_expr` absorbs the
16679                // clause as a no-op (SPG orders text by bytes, which is
16680                // the C collation, so it changes nothing to honour). PG
16681                // still PRINTS it: an explicitly written `"C"` and the
16682                // collation a column inherits are different collation
16683                // OBJECTS even where they sort identically, which is why
16684                // `(a COLLATE "C")` shows on a C-collation database too.
16685                if matches!(
16686                    self.tokens.get(self.pos + 1),
16687                    Some(Token::Ident(w)) if w.eq_ignore_ascii_case("collate")
16688                ) {
16689                    key_collation = match self.tokens.get(self.pos + 2) {
16690                        Some(Token::Ident(n) | Token::QuotedIdent(n) | Token::String(n)) => {
16691                            Some(n.clone())
16692                        }
16693                        _ => None,
16694                    };
16695                }
16696                // v7.39.2 — the clause is read by the LOOKAHEAD above and
16697                // belongs to the KEY, not to the expression. Since
16698                // `COLLATE` became a node, letting `parse_expr` build one
16699                // here put the collation in twice and the key deparsed as
16700                // `(c COLLATE "C" COLLATE "C")`. The ORDER-BY-key channel
16701                // is the same idea and already exists, so this borrows it:
16702                // absorb into the side channel, and the key's own
16703                // lookahead is what carries it.
16704                // v7.39.2 — and the key can only CARRY the byte-order
16705                // spellings. Absorbing into the side channel accepts any
16706                // name, so suppressing the node here without this check
16707                // silently accepted `(name COLLATE "en_US")`, which SPG's
16708                // index cannot honour — a refusal that was doing real
16709                // work, removed by the suppression and put back here.
16710                if let Some(name) = &key_collation {
16711                    let lc = name.to_ascii_lowercase();
16712                    let byte_order = matches!(
16713                        lc.as_str(),
16714                        "c" | "posix" | "default" | "ucs_basic" | "pg_c_utf8"
16715                    );
16716                    let mysql_ok = self.mysql_dialect
16717                        && (lc.ends_with("_ci")
16718                            || lc.ends_with("_bin")
16719                            || lc == "binary"
16720                            || matches!(lc.as_str(), "case_insensitive" | "nocase"));
16721                    if !byte_order && !mysql_ok {
16722                        return Err(self.err(alloc::format!(
16723                            "COLLATE {name:?} is not supported in this position: an index \
16724                             key carries the byte-order spellings only. Declare it on the \
16725                             column (`x text COLLATE {name:?}`) instead"
16726                        )));
16727                    }
16728                }
16729                let saved_key_ctx = self.in_order_by_key;
16730                self.in_order_by_key = true;
16731                let key_expr = self.parse_expr(0);
16732                self.in_order_by_key = saved_key_ctx;
16733                let key_expr = key_expr?;
16734                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16735                    self.err("expression index key must reference at least one column".into())
16736                })?;
16737                (primary, Some(key_expr))
16738            }
16739            // v7.37.43-T4 — parenthesised expression index key
16740            // `CREATE INDEX … ON t ((payload->'bundle'->>'id'))`.
16741            // PG's CREATE INDEX requires the expression to be in
16742            // its own parens to disambiguate function calls from
16743            // column lists, so this `LParen` is the inner open-paren
16744            // of an expression key. parse_expr handles the recursive
16745            // descent and consumes the matching `RParen`.
16746            Token::LParen => {
16747                let key_expr = self.parse_expr(0)?;
16748                let primary = extract_first_column(&key_expr).ok_or_else(|| {
16749                    self.err("expression index key must reference at least one column".into())
16750                })?;
16751                (primary, Some(key_expr))
16752            }
16753            other => {
16754                return Err(self.err(format!(
16755                    "expected column ident or expression, got {other:?}"
16756                )));
16757            }
16758        };
16759        // v7.9.14 — accept extra comma-separated columns inside
16760        // the index key parens (`CREATE INDEX … (a, b, c)`).
16761        // mailrs F2.
16762        //
16763        // v7.39.11 — each extra column's `ASC` / `DESC` / `NULLS FIRST`
16764        // / `NULLS LAST` is KEPT. It used to be parsed and dropped on
16765        // the floor, so `CREATE INDEX i ON t (a, b DESC)` read back from
16766        // `pg_get_indexdef` as `(a, b)`: a dump lost the clause and a
16767        // schema diff saw drift on every run. Reported by sentori
16768        // against 7.39.10, and the same defect round 537 fixed for the
16769        // LEADING column, in the loop right beside it.
16770        let mut extra_columns: Vec<String> = Vec::new();
16771        let mut extra_orders: Vec<crate::ast::IndexColumnOrder> = Vec::new();
16772        // The leading column may also have ASC/DESC after it — and that
16773        // one is the column SPG indexes, so its clause is kept.
16774        let key_order = self.consume_optional_index_column_qualifiers();
16775        while matches!(self.peek(), Token::Comma) {
16776            self.advance();
16777            let extra = self.expect_ident_like()?;
16778            extra_orders.push(self.consume_optional_index_column_qualifiers());
16779            extra_columns.push(extra);
16780        }
16781        if !matches!(self.peek(), Token::RParen) {
16782            return Err(self.err(format!(
16783                "expected ')' after indexed column / expression, got {:?}",
16784                self.peek()
16785            )));
16786        }
16787        self.advance();
16788        // v6.8.0 — optional `INCLUDE (col1, col2, …)` clause for
16789        // index-only-scan annotation. Bare ident (not a reserved
16790        // keyword) so we test by case-insensitive string match.
16791        let included_columns = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("include"))
16792        {
16793            self.advance();
16794            if !matches!(self.peek(), Token::LParen) {
16795                return Err(self.err(format!("expected '(' after INCLUDE, got {:?}", self.peek())));
16796            }
16797            self.advance();
16798            let mut cols = Vec::new();
16799            loop {
16800                cols.push(self.expect_ident_like()?);
16801                match self.peek() {
16802                    Token::Comma => {
16803                        self.advance();
16804                    }
16805                    Token::RParen => {
16806                        self.advance();
16807                        break;
16808                    }
16809                    other => {
16810                        return Err(self.err(format!(
16811                            "expected ',' or ')' in INCLUDE list, got {other:?}"
16812                        )));
16813                    }
16814                }
16815            }
16816            cols
16817        } else {
16818            Vec::new()
16819        };
16820        // v7.11.3 — accept and discard PG `WITH (k = v, ...)` index
16821        // storage parameters. pgvector emits `WITH (lists = N)` for
16822        // ivfflat and `WITH (m = N, ef_construction = M)` for hnsw;
16823        // SPG's HNSW picks its own parameters today (tunable via
16824        // env vars), so the WITH clause is informational and dropped.
16825        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
16826            self.advance();
16827            if !matches!(self.peek(), Token::LParen) {
16828                return Err(self.err(format!(
16829                    "expected '(' after WITH in CREATE INDEX, got {:?}",
16830                    self.peek()
16831                )));
16832            }
16833            self.advance();
16834            loop {
16835                if matches!(self.peek(), Token::RParen) {
16836                    self.advance();
16837                    break;
16838                }
16839                // Drain `key = value` or bare `key` tokens.
16840                let _ = self.advance(); // key
16841                if matches!(self.peek(), Token::Eq) {
16842                    self.advance();
16843                    let _ = self.advance(); // value (int / string / ident)
16844                }
16845                match self.peek() {
16846                    Token::Comma => {
16847                        self.advance();
16848                    }
16849                    Token::RParen => {
16850                        self.advance();
16851                        break;
16852                    }
16853                    other => {
16854                        return Err(self.err(format!(
16855                            "expected ',' or ')' in WITH (…) clause, got {other:?}"
16856                        )));
16857                    }
16858                }
16859            }
16860        }
16861        // v7.39 (read01 round 52) — optional `NULLS [NOT] DISTINCT` (PG 15+),
16862        // which sits between the key list and the WHERE clause.
16863        let mut nulls_not_distinct = false;
16864        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
16865            let n1 = self.tokens.get(self.pos + 1);
16866            let n2 = self.tokens.get(self.pos + 2);
16867            if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
16868                self.advance(); // NULLS
16869                self.advance(); // NOT
16870                self.advance(); // DISTINCT
16871                nulls_not_distinct = true;
16872            } else if matches!(n1, Some(Token::Distinct)) {
16873                self.advance(); // NULLS
16874                self.advance(); // DISTINCT
16875            }
16876        }
16877        // v6.8.1 — optional `WHERE <expr>` partial-index predicate.
16878        let partial_predicate = if matches!(self.peek(), Token::Where) {
16879            self.advance();
16880            Some(self.parse_expr(0)?)
16881        } else {
16882            None
16883        };
16884        // v7.9.29 — UNIQUE on a vector index (HNSW) makes no
16885        // sense: uniqueness over an ANN structure has no clean
16886        // semantics. Reject early. (BRIN UNIQUE is similarly
16887        // meaningless — block both.)
16888        if is_unique && !matches!(method, IndexMethod::BTree) {
16889            return Err(self.err(alloc::format!(
16890                "UNIQUE is only supported on BTree indexes, got USING {:?}",
16891                method
16892            )));
16893        }
16894        Ok(Statement::CreateIndex(CreateIndexStatement {
16895            concurrently,
16896            name,
16897            key_order,
16898            key_collation,
16899            table,
16900            column,
16901            nulls_not_distinct,
16902            method,
16903            if_not_exists,
16904            included_columns,
16905            partial_predicate,
16906            extra_columns: extra_columns.clone(),
16907            extra_orders: extra_orders.clone(),
16908            expression,
16909            is_unique,
16910            opclass,
16911            method_name,
16912        }))
16913    }
16914
16915    /// v7.6.0 — wraps `parse_column_def` and consumes an optional
16916    /// column-level `REFERENCES ...` clause. The trailing FK is
16917    /// normalised into table-level shape (single-element columns +
16918    /// parent_columns) so the engine sees one uniform constraint list.
16919    fn parse_column_def_with_fk(
16920        &mut self,
16921    ) -> Result<(ColumnDef, Option<ForeignKeyConstraint>), ParseError> {
16922        let col = self.parse_column_def()?;
16923        // v7.39 (round 308, V29) — an explicitly named inline FK:
16924        // `col INT CONSTRAINT fk_a REFERENCES tbl(pcol)`. The column-def
16925        // loop leaves this spelling intact precisely so the name can be
16926        // kept here; PG reports it in violation messages and matches it
16927        // in `SET CONSTRAINTS`.
16928        let declared_name = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint"))
16929        {
16930            self.advance();
16931            Some(self.expect_ident_like()?)
16932        } else {
16933            None
16934        };
16935        // Inline form: `col INT REFERENCES tbl(pcol) [ON DELETE ...] [ON UPDATE ...]`.
16936        let inline_references = matches!(
16937            self.peek(),
16938            Token::Ident(s) if s.eq_ignore_ascii_case("references")
16939        );
16940        if !inline_references {
16941            return Ok((col, None));
16942        }
16943        let (
16944            parent_table,
16945            parent_columns,
16946            on_delete,
16947            on_update,
16948            match_type,
16949            deferrable,
16950            initially_deferred,
16951        ) = self.parse_references_tail(1)?;
16952        let fk = ForeignKeyConstraint {
16953            name: declared_name,
16954            columns: vec![col.name.clone()],
16955            parent_table,
16956            parent_columns,
16957            on_delete,
16958            on_update,
16959            match_type,
16960            deferrable,
16961            initially_deferred,
16962        };
16963        Ok((col, Some(fk)))
16964    }
16965
16966    /// v7.13.0 — parse a column type (consuming the type ident and
16967    /// any trailing parameters / `[]`), without surrounding column
16968    /// constraints. Used by ALTER COLUMN TYPE (mailrs round-5 G8).
16969    /// Returns the resolved `ColumnTypeName` plus implied
16970    /// `(auto_increment, not_null)` flags from PG SERIAL family
16971    /// shorthands — callers that don't expect those (ALTER COLUMN
16972    /// TYPE) can discard them.
16973    fn parse_column_type_name(&mut self) -> Result<ColumnTypeName, ParseError> {
16974        let (ty, _, _, _, _, _, _, _, _, _, _, _, _, _) = self.parse_type_with_implied_flags()?;
16975        Ok(ty)
16976    }
16977
16978    #[allow(clippy::type_complexity)]
16979    fn parse_type_with_implied_flags(
16980        &mut self,
16981    ) -> Result<
16982        (
16983            ColumnTypeName,
16984            bool,
16985            bool,
16986            Option<String>,
16987            Collation,
16988            // v7.39 (round 370, M4 P4a) — was `COLLATE` written explicitly?
16989            bool,
16990            // v7.39 (round 676) — the collation NAME as written, which the
16991            // `Collation` enum above cannot carry.
16992            Option<String>,
16993            bool,
16994            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM variant
16995            // list captured at type-parse time. None for all
16996            // non-ENUM types.
16997            Option<Vec<String>>,
16998            // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant
16999            // list. Distinct from ENUM (subset semantics).
17000            Option<Vec<String>>,
17001            // v7.39 (round 386, epic P1) — declared TINYINT / MEDIUMINT
17002            // width, lost when the type collapses to SmallInt / Int.
17003            Option<MysqlIntWidth>,
17004            // v7.39 (round 424) — declared fractional-seconds precision of a
17005            // MySQL temporal column (bare spelling = 0). None under PG.
17006            Option<u8>,
17007            // v7.39.2 — written `TIMESTAMP` rather than `DATETIME`. The
17008            // two are different types on MySQL and SPG stores both as
17009            // `Timestamp`, so the spelling has to travel separately or
17010            // a dump silently rewrites the column.
17011            bool,
17012            // v7.39.3 — a MySQL `FLOAT(m,d)` / `DOUBLE(m,d)` pair. Not a
17013            // display hint: it rounds on write.
17014            Option<(u8, u8)>,
17015        ),
17016        ParseError,
17017    > {
17018        let mut ty_ident = match self.advance() {
17019            Token::Ident(s) => s,
17020            // v7.37.5 β-P2 — `INTERVAL` lexes as a reserved keyword
17021            // (Token::Interval) since v7.9.25 to drive the `INTERVAL
17022            // '<span>'` literal grammar. As a column type it lands
17023            // here directly; downstream resolution still uses the
17024            // canonical lowercase string.
17025            Token::Interval => "interval".to_string(),
17026            other => {
17027                return Err(ParseError {
17028                    message: format!("expected column type, got {other:?}"),
17029                    token_pos: self.consumed_pos(),
17030                });
17031            }
17032        };
17033        // v7.22 (mailrs round-13 gap 4) — schema-qualified type names:
17034        // pg_dump qualifies extension types (`public.vector(1024)`).
17035        // SPG is single-namespace; drop the schema and resolve the
17036        // bare type — same treatment table names already get.
17037        while matches!(self.peek(), Token::Dot) {
17038            self.advance();
17039            ty_ident = self.expect_ident_like()?;
17040        }
17041        let mut implied_auto_increment = false;
17042        let mut implied_not_null = false;
17043        let mut user_type_ref: Option<String> = None;
17044        // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM('a','b','c')
17045        // value list, captured here and bubbled up through the
17046        // ColumnDef so the engine can attach it to the column
17047        // schema (and validate INSERT cells against it).
17048        let mut inline_enum_variants: Option<Vec<String>> = None;
17049        // v7.17.0 Phase 3.P0-37 — MySQL inline SET variant list.
17050        let mut inline_set_variants: Option<Vec<String>> = None;
17051        // v7.39 (round 386, type-fidelity epic P1) — the declared MySQL
17052        // narrow-int width (TINYINT / MEDIUMINT), captured before the type
17053        // collapses to SmallInt / Int. Only under the MySQL dialect.
17054        let mut mysql_int_width: Option<MysqlIntWidth> = None;
17055        // v7.39 (round 424) — the declared fractional-seconds precision of a
17056        // MySQL temporal column. Set by the temporal arms below; stays None
17057        // for PG (whose temporal columns keep full microseconds).
17058        let mut mysql_fsp: Option<u8> = None;
17059        let mut mysql_declared_timestamp = false;
17060        let mut mysql_float_md: Option<(u8, u8)> = None;
17061        let mut ty = match ty_ident.as_str() {
17062            // PG SERIAL family. Implies NOT NULL + AUTO_INCREMENT.
17063            "smallserial" | "serial2" => {
17064                implied_auto_increment = true;
17065                implied_not_null = true;
17066                ColumnTypeName::SmallInt
17067            }
17068            "serial" | "serial4" => {
17069                implied_auto_increment = true;
17070                implied_not_null = true;
17071                ColumnTypeName::Int
17072            }
17073            "bigserial" | "serial8" => {
17074                implied_auto_increment = true;
17075                implied_not_null = true;
17076                ColumnTypeName::BigInt
17077            }
17078            // MySQL flavours we accept by aliasing to the closest SPG
17079            // type. TINYINT covers MySQL's i8 — held inside SMALLINT
17080            // since SPG doesn't have a dedicated i8. MEDIUMINT (MySQL
17081            // 24-bit) → INT. UNSIGNED modifiers are consumed below
17082            // without semantic effect.
17083            // v7.38 (read01 P4.19-sibling) — `int2` / `int4` / `int8` are
17084            // PG's internal type names; pg_dump and hand-written PG schemas
17085            // use them interchangeably with smallint / int / bigint (the cast
17086            // path already accepted them, only the column grammar didn't).
17087            "smallint" | "int2" => {
17088                // v7.14.0 — MySQL display-width on integers
17089                // (`SMALLINT(5)`, `INT(11)`, `BIGINT(20)`). The
17090                // parenthesised number is purely cosmetic — it
17091                // doesn't change storage. Accept + discard.
17092                self.consume_optional_paren_size();
17093                ColumnTypeName::SmallInt
17094            }
17095            // v7.17.0 Phase 4.3 — MySQL `TINYINT(1)` is the
17096            // canonical encoding for BOOLEAN. Every MySQL driver
17097            // (JDBC `tinyInt1isBit=true`, PHP `mysql_field_type`,
17098            // .NET `MySqlConnection`, sqlx) maps it to bit. Pre-
17099            // 4.3 SPG classified TINYINT(1) as SmallInt, which
17100            // gave the customer i16-shaped values where the app
17101            // expected bool — a Tier-A silent type drift on
17102            // mysqldump restores. Now: `TINYINT(1)` → Bool;
17103            // `TINYINT` (no width) and `TINYINT(N)` for N ≠ 1
17104            // stay SmallInt (the legacy width-agnostic path).
17105            "tinyint" => {
17106                let width = self.peek_optional_paren_size_value();
17107                self.consume_optional_paren_size();
17108                if width == Some(1) {
17109                    ColumnTypeName::Bool
17110                } else {
17111                    // v7.39 (round 386, epic P1) — TINYINT is i8; record the
17112                    // lost width so the write path can enforce -128..127.
17113                    if self.mysql_dialect {
17114                        mysql_int_width = Some(MysqlIntWidth::Tiny);
17115                    }
17116                    ColumnTypeName::SmallInt
17117                }
17118            }
17119            "mediumint" => {
17120                self.consume_optional_paren_size();
17121                // v7.39 (round 386, epic P1) — MEDIUMINT is 24-bit; record it.
17122                if self.mysql_dialect {
17123                    mysql_int_width = Some(MysqlIntWidth::Medium);
17124                }
17125                ColumnTypeName::Int
17126            }
17127            "int" | "integer" | "int4" => {
17128                self.consume_optional_paren_size();
17129                ColumnTypeName::Int
17130            }
17131            "bigint" | "int8" => {
17132                self.consume_optional_paren_size();
17133                ColumnTypeName::BigInt
17134            }
17135            // v7.13.0 — `DOUBLE PRECISION` (PG canonical spelling)
17136            // (mailrs round-5 G6). Consume the optional `PRECISION`
17137            // tail when the type keyword was `double` / `DOUBLE`.
17138            //
17139            // v7.39 (round 269) — REAL is 32-bit, not "the same as our
17140            // FLOAT". `FLOAT(p)` picks the width the way PG does:
17141            // p in 1..=24 is real, 25..=53 is double precision, and
17142            // anything else is an error.
17143            "float" | "double" | "real" => {
17144                if ty_ident.eq_ignore_ascii_case("double")
17145                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("precision"))
17146                {
17147                    self.advance();
17148                }
17149                if ty_ident.eq_ignore_ascii_case("real") {
17150                    // v7.39 (round 274) — the two dialects genuinely
17151                    // disagree: PG's REAL is 4-byte, MySQL's REAL is a
17152                    // synonym for DOUBLE (8-byte). Round 269 made REAL
17153                    // 32-bit globally and thereby narrowed the stored
17154                    // precision of every MySQL REAL column.
17155                    if self.mysql_dialect {
17156                        ColumnTypeName::Float
17157                    } else {
17158                        ColumnTypeName::Real
17159                    }
17160                } else if self.mysql_dialect
17161                    && matches!(self.peek(), Token::LParen)
17162                    && self.peek_paren_has_comma()
17163                {
17164                    // v7.39 (round 360) — MySQL's `FLOAT(m,d)` / `DOUBLE(m,d)`
17165                    // display form (`FLOAT(10,2)`), which PG has no
17166                    // equivalent of. It was `syntax error at or near ","`,
17167                    // so the whole CREATE failed.
17168                    //
17169                    // v7.39.2 — the guard said `float` while the comment
17170                    // said both, so `DOUBLE(10,2)` — which every legacy
17171                    // MySQL schema uses for money — still failed the
17172                    // whole CREATE with `syntax error at or near "("`.
17173                    // Measured on 9.7.2: both forms are accepted, and the
17174                    // digits are NOT a display hint, they round on write
17175                    // (3.14159265358979 into either stores 3.14). The
17176                    // rounding is recorded as a residual; accepting the
17177                    // syntax and keeping the width is the half this
17178                    // change makes.
17179                    // v7.39.3 — keep the pair. The digits are not a
17180                    // display hint: MySQL 9.7.2 ROUNDS on write and
17181                    // refuses a value wider than `m` (errno 1264), so a
17182                    // column declared for money held more precision here
17183                    // than its schema said.
17184                    let (m, d) = self.parse_optional_numeric_params()?;
17185                    mysql_float_md = Some((
17186                        u8::try_from(m).unwrap_or(u8::MAX),
17187                        u8::try_from(d.max(0)).unwrap_or(u8::MAX),
17188                    ));
17189                    if ty_ident.eq_ignore_ascii_case("float") {
17190                        ColumnTypeName::Real
17191                    } else {
17192                        ColumnTypeName::Float
17193                    }
17194                } else if ty_ident.eq_ignore_ascii_case("float")
17195                    && matches!(self.peek(), Token::LParen)
17196                {
17197                    // PG words the two bounds differently, and
17198                    // parse_paren_size already rejects a zero.
17199                    let p = self.parse_paren_size("FLOAT")?;
17200                    if p > 53 {
17201                        return Err(self.err(String::from(
17202                            "precision for type float must be less than 54 bits",
17203                        )));
17204                    }
17205                    if p <= 24 {
17206                        ColumnTypeName::Real
17207                    } else {
17208                        ColumnTypeName::Float
17209                    }
17210                } else if ty_ident.eq_ignore_ascii_case("float") && self.mysql_dialect {
17211                    // v7.39.2 — MySQL's bare FLOAT is FOUR bytes; PG's is
17212                    // eight (it is `float8`'s spelling there). SPG used
17213                    // PG's for both, so a MySQL FLOAT column silently
17214                    // kept more precision than MySQL does — measured,
17215                    // 3.14159265358979 comes back as 3.14159 there and
17216                    // came back whole here — and reported itself as
17217                    // `double` to every reflection.
17218                    //
17219                    // This is the mirror of the REAL split above: the
17220                    // two dialects disagree about which spelling means
17221                    // which width, and one of them was already honoured.
17222                    ColumnTypeName::Real
17223                } else {
17224                    ColumnTypeName::Float
17225                }
17226            }
17227            // v7.13.0 — `FLOAT8` (PG short form) maps the same as FLOAT.
17228            "float4" => ColumnTypeName::Real,
17229            "float8" => ColumnTypeName::Float,
17230            "text" => ColumnTypeName::Text,
17231            // v7.39 (round 360) — MySQL's sized TEXT and BLOB families.
17232            // `LONGTEXT`, `BLOB` and `VARBINARY` appear in nearly every
17233            // real MySQL schema and NONE of them existed: the CREATE
17234            // failed outright with `type "blob" does not exist`, so the
17235            // table was never made. The sizes differ only in MySQL's
17236            // maximum length, which SPG does not cap, so they collapse
17237            // onto TEXT and BYTEA the way the unsized spellings do.
17238            "tinytext" | "mediumtext" | "longtext" => ColumnTypeName::Text,
17239            "blob" | "tinyblob" | "mediumblob" | "longblob" => ColumnTypeName::Bytes,
17240            // `VARBINARY(n)` / `BINARY(n)` — a length that SPG does not
17241            // enforce, consumed so the declaration parses.
17242            "varbinary" | "binary" => {
17243                self.consume_optional_paren_size();
17244                ColumnTypeName::Bytes
17245            }
17246            "name" => ColumnTypeName::Name,
17247            "xid" => ColumnTypeName::Xid,
17248            "oid" => ColumnTypeName::Oid,
17249            "xid8" => ColumnTypeName::Xid8,
17250            "bool" | "boolean" => ColumnTypeName::Bool,
17251            // v7.39 (round 620) — an UNBOUNDED `varchar` is the same type as
17252            // an unbounded `character varying`, which the arm below has always
17253            // read as text. Only the short spelling demanded a length, so
17254            // `CREATE TABLE t (x VARCHAR)` — as ordinary a line of DDL as
17255            // there is — failed on `VARCHAR type requires (N)` while the long
17256            // spelling of the same thing was accepted. The same asymmetry
17257            // round 613 closed on the CAST side, here on the DDL side.
17258            "varchar" => {
17259                if matches!(self.peek(), Token::LParen) {
17260                    ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17261                } else {
17262                    ColumnTypeName::Text
17263                }
17264            }
17265            // v7.39 (bpchar epic) — bare `char` = char(1), same as bare
17266            // `character` below (SQL standard).
17267            "char" => {
17268                if matches!(self.peek(), Token::LParen) {
17269                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17270                } else {
17271                    ColumnTypeName::Char(1)
17272                }
17273            }
17274            // pg_dump's canonical spellings: `character varying(n)` = varchar,
17275            // `character(n)` = char, bare `character` = char(1). Unbounded
17276            // `character varying` maps to text.
17277            "character" => {
17278                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("varying")) {
17279                    self.advance();
17280                    if matches!(self.peek(), Token::LParen) {
17281                        ColumnTypeName::Varchar(self.parse_paren_size("VARCHAR")?)
17282                    } else {
17283                        ColumnTypeName::Text
17284                    }
17285                } else if matches!(self.peek(), Token::LParen) {
17286                    ColumnTypeName::Char(self.parse_paren_size("CHAR")?)
17287                } else {
17288                    ColumnTypeName::Char(1)
17289                }
17290            }
17291            "vector" => {
17292                let dim = self.parse_paren_size("VECTOR")?;
17293                let encoding = self.parse_optional_vector_encoding()?;
17294                ColumnTypeName::Vector { dim, encoding }
17295            }
17296            // v7.39 (round 345, M5) — `DECIMAL` and `DEC` are the SQL
17297            // standard's own spellings of NUMERIC, and PG 18.4 accepts both
17298            // (measured: `DECIMAL(10,2)` and `DEC(5,1)` both report as
17299            // `numeric`). Only `NUMERIC` parsed, so `CREATE TABLE t (a
17300            // DECIMAL(10,2))` — how nearly every money column is written,
17301            // in either dialect — was a syntax error and the table was
17302            // never created. `FIXED` is MySQL's alias alone, so it is
17303            // taken only in that dialect.
17304            "numeric" | "decimal" | "dec" => {
17305                let (precision, scale) = self.parse_optional_numeric_params()?;
17306                ColumnTypeName::Numeric(precision, scale)
17307            }
17308            "fixed" if self.mysql_dialect => {
17309                let (precision, scale) = self.parse_optional_numeric_params()?;
17310                ColumnTypeName::Numeric(precision, scale)
17311            }
17312            "date" => ColumnTypeName::Date,
17313            // MySQL's `DATETIME` is the same domain as standard
17314            // `TIMESTAMP` — accept both spellings.
17315            "timestamp" | "datetime" => {
17316                // pg_dump emits `TIMESTAMP(6) WITH TIME ZONE` — the optional
17317                // fractional-seconds precision comes BEFORE the `WITH/WITHOUT
17318                // TIME ZONE` clause, so consume it first.
17319                // v7.39 (round 424) — under MySQL the precision is SEMANTIC
17320                // (it truncates on write and pads on render), so capture it;
17321                // a bare spelling means precision 0 there. PG stores µs always
17322                // and keeps `None`.
17323                let n = self.take_optional_paren_size();
17324                if self.mysql_dialect {
17325                    mysql_fsp = Some(n.unwrap_or(0).min(6));
17326                    // v7.39.2 — remember WHICH spelling was written.
17327                    // MySQL and MariaDB keep `timestamp` and `datetime`
17328                    // apart everywhere a client can read the type back,
17329                    // and SPG reported `datetime` for both — so a dump
17330                    // and reload silently changed the column's declared
17331                    // type, and MySQL's TIMESTAMP is not DATETIME (a
17332                    // different range, and UTC conversion on the way in
17333                    // and out).
17334                    mysql_declared_timestamp = ty_ident.eq_ignore_ascii_case("timestamp");
17335                }
17336                // v7.14.0 — PG canonical `TIMESTAMP WITH TIME ZONE`
17337                // / `TIMESTAMP WITHOUT TIME ZONE`. pg_dump emits
17338                // the full form. SPG canonicalises:
17339                //   - WITH TIME ZONE    → Timestamptz
17340                //   - WITHOUT TIME ZONE → Timestamp
17341                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17342                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17343                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17344                {
17345                    self.advance(); // WITH
17346                    self.advance(); // TIME
17347                    self.advance(); // ZONE
17348                    ColumnTypeName::Timestamptz
17349                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17350                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17351                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17352                {
17353                    self.advance(); // WITHOUT
17354                    self.advance(); // TIME
17355                    self.advance(); // ZONE
17356                    ColumnTypeName::Timestamp
17357                } else {
17358                    // A second `(precision)` cannot legally follow, but the
17359                    // old grammar tolerated it; keep that tolerance.
17360                    self.consume_optional_paren_size();
17361                    ColumnTypeName::Timestamp
17362                }
17363            }
17364            // v7.9.2 — `TIMESTAMPTZ` and full PG spelling
17365            // `TIMESTAMP WITH TIME ZONE`. Same storage as TIMESTAMP;
17366            // only PG-wire OID differs.
17367            "timestamptz" => {
17368                self.consume_optional_paren_size();
17369                ColumnTypeName::Timestamptz
17370            }
17371            // v4.9: JSON / JSONB. Stored as raw text — no parse-time
17372            // validation. We accept the JSONB spelling too because
17373            // most PG clients default to it; SPG doesn't distinguish
17374            // the two (no path-operator perf advantage to model).
17375            "json" => ColumnTypeName::Json,
17376            "jsonb" => ColumnTypeName::Jsonb,
17377            // v7.10.4 — PG `BYTEA` and the SPG `BYTES` alias both
17378            // surface here. Same storage shape; mapping happens at
17379            // the engine side via the ColumnTypeName → DataType
17380            // resolver. Literal forms are handled at coerce_value
17381            // time so the lexer stays untouched.
17382            "bytea" | "bytes" => ColumnTypeName::Bytes,
17383            // v7.17.0 Phase 7 — PG network address types
17384            // v7.17.0 had a Text-backed fallback here for
17385            // `inet` / `cidr` / `macaddr`. v7.37.5 ζ-A promoted
17386            // each to a first-class type; the keywords are
17387            // bound below in the ζ-A block.
17388            // v7.12.0 — PG full-text search types. mailrs G-CRIT-3.
17389            // The actual `to_tsvector` / `@@` / `ts_rank` surface
17390            // arrives in v7.12.1+; the type itself loads here so
17391            // mailrs's `scripts/init-schema.sql` runs unmodified.
17392            "tsvector" => ColumnTypeName::TsVector,
17393            "tsquery" => ColumnTypeName::TsQuery,
17394            // v7.17.0 — PG `UUID`. Wire OID 2950. The drop-in PG
17395            // surface for Django / Rails / Hibernate's default
17396            // PK pattern.
17397            "uuid" => ColumnTypeName::Uuid,
17398            // v7.37.5 β-P2 — PG `INTERVAL` as a column type.
17399            // Storage = three-field {months, days, micros}, catalog
17400            // tag 34, FILE_VERSION 48+, wire OID 1186. Prior to this
17401            // line `INTERVAL` was parser-rejected at CREATE TABLE.
17402            "interval" => {
17403                // pg_dump emits field-qualified forms like `INTERVAL DAY TO
17404                // SECOND` and an optional `(p)` precision. SPG stores the full
17405                // {months,days,micros}; consume + ignore the qualifier/precision.
17406                while matches!(self.peek(), Token::To)
17407                    || matches!(self.peek(), Token::Ident(s) if matches!(
17408                        s.to_ascii_lowercase().as_str(),
17409                        "year" | "month" | "day" | "hour" | "minute" | "second"
17410                    ))
17411                {
17412                    self.advance();
17413                }
17414                self.consume_optional_paren_size();
17415                ColumnTypeName::Interval
17416            }
17417            // v7.17.0 Phase 3.P0-32 — PG `TIME` (without time zone).
17418            // i64 microseconds since 00:00:00. Wire OID 1083.
17419            // pg_dump emits `TIME(6)` and `TIME(6) WITH TIME ZONE`.
17420            "time" => {
17421                // v7.39 (round 424) — MySQL TIME carries a semantic
17422                // fractional-seconds precision, bare meaning 0.
17423                let n = self.take_optional_paren_size();
17424                if self.mysql_dialect {
17425                    mysql_fsp = Some(n.unwrap_or(0).min(6));
17426                }
17427                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
17428                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17429                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17430                {
17431                    self.advance();
17432                    self.advance();
17433                    self.advance();
17434                    ColumnTypeName::TimeTz
17435                } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("without"))
17436                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
17437                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
17438                {
17439                    self.advance();
17440                    self.advance();
17441                    self.advance();
17442                    ColumnTypeName::Time
17443                } else {
17444                    ColumnTypeName::Time
17445                }
17446            }
17447            // v7.17.0 Phase 3.P0-33 — MySQL `YEAR`. u16 in
17448            // 1901..=2155 + zero-year sentinel 0. Wire = INT4.
17449            "year" => ColumnTypeName::Year,
17450            // v7.17.0 Phase 3.P0-34 — PG `TIMETZ` / `TIME WITH
17451            // TIME ZONE`. i64 us + i32 offset_secs. Wire OID 1266.
17452            "timetz" => ColumnTypeName::TimeTz,
17453            // v7.17.0 Phase 3.P0-35 — PG `MONEY` — i64 cents.
17454            // Wire OID 790.
17455            "money" => ColumnTypeName::Money,
17456            // v7.17.0 Phase 3.P0-38 — PG range types.
17457            "int4range" => ColumnTypeName::Range(RangeKindAst::Int4),
17458            "int8range" => ColumnTypeName::Range(RangeKindAst::Int8),
17459            "numrange" => ColumnTypeName::Range(RangeKindAst::Num),
17460            "tsrange" => ColumnTypeName::Range(RangeKindAst::Ts),
17461            "tstzrange" => ColumnTypeName::Range(RangeKindAst::TsTz),
17462            "daterange" => ColumnTypeName::Range(RangeKindAst::Date),
17463            // v7.37.5 δ — PG 14+ multirange keywords.
17464            "int4multirange" => ColumnTypeName::Multirange(RangeKindAst::Int4),
17465            "int8multirange" => ColumnTypeName::Multirange(RangeKindAst::Int8),
17466            "nummultirange" => ColumnTypeName::Multirange(RangeKindAst::Num),
17467            "tsmultirange" => ColumnTypeName::Multirange(RangeKindAst::Ts),
17468            "tstzmultirange" => ColumnTypeName::Multirange(RangeKindAst::TsTz),
17469            "datemultirange" => ColumnTypeName::Multirange(RangeKindAst::Date),
17470            // v7.37.5 ε — PG geometry scalar keywords.
17471            "point" => ColumnTypeName::Point,
17472            "lseg" => ColumnTypeName::Lseg,
17473            "path" => ColumnTypeName::Path,
17474            "box" => ColumnTypeName::PgBox,
17475            "polygon" => ColumnTypeName::Polygon,
17476            "line" => ColumnTypeName::Line,
17477            "circle" => ColumnTypeName::Circle,
17478            // v7.37.5 ζ-A — network / bit / xml / "char" keywords.
17479            "inet" => ColumnTypeName::Inet,
17480            "cidr" => ColumnTypeName::Cidr,
17481            "macaddr" => ColumnTypeName::Macaddr,
17482            "macaddr8" => ColumnTypeName::Macaddr8,
17483            // `bit`, `bit(N)`, `bit varying`, `bit varying(N)`. SPG carries the
17484            // width in the value, so the optional `(N)` typmod is accepted and
17485            // ignored (the column stores whatever width it's given).
17486            "bit" => {
17487                let varying = matches!(
17488                    self.peek(),
17489                    Token::Ident(k) if k.eq_ignore_ascii_case("varying")
17490                );
17491                if varying {
17492                    self.advance();
17493                }
17494                // v7.39 (round 281) — the length used to be parsed and
17495                // dropped, so `bit(3)` accepted a five-bit string.
17496                let n = if matches!(self.peek(), Token::LParen) {
17497                    self.parse_paren_size("BIT")?
17498                } else {
17499                    0
17500                };
17501                if varying {
17502                    ColumnTypeName::BitVarying(n)
17503                } else {
17504                    ColumnTypeName::Bit(n)
17505                }
17506            }
17507            "varbit" => {
17508                let n = if matches!(self.peek(), Token::LParen) {
17509                    self.parse_paren_size("VARBIT")?
17510                } else {
17511                    0
17512                };
17513                ColumnTypeName::BitVarying(n)
17514            }
17515            "xml" => ColumnTypeName::Xml,
17516            // v7.17.0 Phase 3.P0-39 — PG hstore extension type.
17517            "hstore" => ColumnTypeName::Hstore,
17518            // v7.17.0 Phase 3.P0-36 — MySQL inline ENUM
17519            // `ENUM('a','b','c')`. Storage is TEXT; the value
17520            // list lands on `inline_enum_variants` for the
17521            // engine to validate INSERT cells against. Empty
17522            // value list is a parse error (matches MySQL).
17523            "enum" => {
17524                // Expect the opening `(`.
17525                if !matches!(self.peek(), Token::LParen) {
17526                    return Err(self.err(alloc::format!(
17527                        "expected '(' after ENUM, 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                                "ENUM(...) 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 ENUM(...), got {other:?}"
17554                            )));
17555                        }
17556                    }
17557                }
17558                if variants.is_empty() {
17559                    return Err(self.err("ENUM(...) must declare at least one variant".into()));
17560                }
17561                inline_enum_variants = Some(variants);
17562                // Storage is plain TEXT; the variant list lives on
17563                // the ColumnSchema side.
17564                ColumnTypeName::Text
17565            }
17566            // v7.17.0 Phase 3.P0-37 — MySQL inline SET
17567            // `SET('a','b','c')`. Same parse shape as ENUM;
17568            // semantics differ (subset rather than pick-one).
17569            "set" => {
17570                if !matches!(self.peek(), Token::LParen) {
17571                    return Err(self.err(alloc::format!(
17572                        "expected '(' after SET, got {:?}",
17573                        self.peek()
17574                    )));
17575                }
17576                self.advance();
17577                let mut variants: Vec<String> = Vec::new();
17578                loop {
17579                    match self.advance() {
17580                        Token::String(s) => variants.push(s),
17581                        other => {
17582                            return Err(self.err(alloc::format!(
17583                                "SET(...) expects string literal variants, got {other:?}"
17584                            )));
17585                        }
17586                    }
17587                    match self.peek() {
17588                        Token::Comma => {
17589                            self.advance();
17590                            continue;
17591                        }
17592                        Token::RParen => {
17593                            self.advance();
17594                            break;
17595                        }
17596                        other => {
17597                            return Err(self.err(alloc::format!(
17598                                "expected ',' or ')' in SET(...), got {other:?}"
17599                            )));
17600                        }
17601                    }
17602                }
17603                if variants.is_empty() {
17604                    return Err(self.err("SET(...) must declare at least one variant".into()));
17605                }
17606                inline_set_variants = Some(variants);
17607                ColumnTypeName::Text
17608            }
17609            _other => {
17610                // v7.17.0 Phase 1.4 — unknown ident → defer
17611                // resolution to the engine. Stored as Text in
17612                // ColumnTypeName + the original name carried as
17613                // `user_type_ref` so CREATE TABLE can look up
17614                // user-defined enum / domain types.
17615                user_type_ref = Some(ty_ident.clone());
17616                ColumnTypeName::Text
17617            }
17618        };
17619        // v7.17.0 Phase 4.4 — MySQL's `UNSIGNED` modifier sits
17620        // right after the type keyword. Pre-4.4 SPG consumed +
17621        // discarded the keyword, leaving a customer column
17622        // declared `id INT UNSIGNED NOT NULL` silently accepting
17623        // negative values — a Tier-A correctness drift where
17624        // application invariants (auto-increment-IDs never
17625        // negative) silently broke on cutover. Now: capture as
17626        // a column flag, persist on the schema, enforce at
17627        // INSERT / UPDATE time.
17628        let is_unsigned = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unsigned"))
17629        {
17630            self.advance();
17631            true
17632        } else {
17633            false
17634        };
17635        // v7.14.0 — mysqldump emits `<type> CHARACTER SET <name>` and
17636        // `<type> COLLATE <name>` post-fixes on text columns. SPG
17637        // stores text as UTF-8 always so CHARACTER SET is still a
17638        // no-op. v7.17.0 Phase 2.5 — COLLATE no longer drops the
17639        // name: it gets classified into a `Collation` variant the
17640        // engine consults at WHERE-eval time. PG `default` /
17641        // `pg_catalog.default` / `C` / `POSIX` collations all
17642        // resolve to `Binary` (the prior behaviour); `_ci` /
17643        // `case_insensitive` / `nocase` shift to CaseInsensitive.
17644        // The schema-qualifier form (`pg_catalog.default`) lexes
17645        // as `Ident '.' Ident` — peek for the `.` and consume both
17646        // halves so it's treated as one collation name. PG's
17647        // `IDENT.IDENT` collation form (which can appear here) is
17648        // resolved by Collation::from_collation_name on the bare
17649        // identifier after the dot.
17650        let mut collation = Collation::Binary;
17651        // v7.39 (round 370, M4 P4a) — whether an explicit `COLLATE <name>`
17652        // clause was written. The engine needs this to tell an explicit
17653        // `COLLATE utf8mb4_bin` (byte-wise) apart from a column with no
17654        // clause at all: both resolve to `Collation::Binary`, but under the
17655        // MySQL dialect the latter takes the folding default collation.
17656        let mut collation_explicit = false;
17657        let mut collation_name: Option<alloc::string::String> = None;
17658        loop {
17659            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("character"))
17660                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("set"))
17661            {
17662                self.advance(); // CHARACTER
17663                self.advance(); // SET
17664                if matches!(
17665                    self.peek(),
17666                    Token::Ident(_) | Token::QuotedIdent(_) | Token::String(_)
17667                ) {
17668                    self.advance();
17669                }
17670                continue;
17671            }
17672            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
17673                self.advance(); // COLLATE
17674                // Accept Ident / QuotedIdent / String AND the
17675                // keyword-tokenised `Default` (PG `pg_catalog.default`
17676                // and bare `DEFAULT` collation names — `default` is a
17677                // reserved word so the lexer hands back Token::Default
17678                // not Token::Ident).
17679                let read_collation_atom = |this: &mut Self| -> Option<alloc::string::String> {
17680                    match this.peek().clone() {
17681                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => {
17682                            this.advance();
17683                            Some(s)
17684                        }
17685                        Token::Default => {
17686                            this.advance();
17687                            Some(alloc::string::String::from("default"))
17688                        }
17689                        _ => None,
17690                    }
17691                };
17692                let raw = if let Some(head) = read_collation_atom(self) {
17693                    // Schema-qualified PG form: `pg_catalog.default`.
17694                    if matches!(self.peek(), Token::Dot) {
17695                        self.advance();
17696                        let tail = read_collation_atom(self).unwrap_or_default();
17697                        alloc::format!("{head}.{tail}")
17698                    } else {
17699                        head
17700                    }
17701                } else {
17702                    alloc::string::String::new()
17703                };
17704                if !raw.is_empty() {
17705                    collation_explicit = true;
17706                    // v7.39 (round 676) — keep the name too. The enum below
17707                    // folds C / POSIX / en_US / default into one value, and
17708                    // `pg_attribute.attcollation` has to tell them apart.
17709                    // The schema qualifier goes: PG's `pg_catalog.default`
17710                    // and a bare `default` name the same collation.
17711                    // v7.39 (round 679) — strip a SCHEMA qualifier, not an
17712                    // encoding suffix. Round 676 used `rsplit('.')` for
17713                    // both, and `COLLATE "en_US.utf8"` came out as `utf8`:
17714                    // PG writes `pg_catalog.default` (qualifier) and
17715                    // `en_US.utf8` (locale + encoding) with the same
17716                    // separator. Only `pg_catalog.` is a qualifier, and it
17717                    // is the only one PG's own dumps emit.
17718                    let bare = raw.trim_matches(|c: char| c == '"' || c == '\'');
17719                    let bare = bare.strip_prefix("pg_catalog.").unwrap_or(bare);
17720                    collation_name = Some(alloc::string::String::from(bare));
17721                    let parsed = Collation::from_collation_name(&raw);
17722                    // Last COLLATE clause wins, but `Binary` from a
17723                    // bare keyword like `default` should not
17724                    // silently downgrade a stronger one set earlier
17725                    // on the same column. v7.17 only ships one
17726                    // non-Binary variant so a simple OR is enough.
17727                    if parsed != Collation::Binary {
17728                        collation = parsed;
17729                    }
17730                }
17731                continue;
17732            }
17733            break;
17734        }
17735        // v7.10.10 — postfix `[]` widens the base type to its array
17736        // type. PG accepts `TYPE[]` after any base type and so does
17737        // SPG now (round-753 probe: INT[] / NUMERIC[] / TIMESTAMP[]
17738        // all through; the old "only TEXT[]" note was stale).
17739        if matches!(self.peek(), Token::LBracket) {
17740            self.advance();
17741            if !matches!(self.peek(), Token::RBracket) {
17742                return Err(self.err(alloc::format!(
17743                    "TEXT[] takes no dimension; got {:?}",
17744                    self.peek()
17745                )));
17746            }
17747            self.advance();
17748            // v7.11.13 — widened to INT[] and BIGINT[] in addition
17749            // to TEXT[]. Other base types (BOOL[], NUMERIC[], etc.)
17750            // still error here.
17751            ty = match ty {
17752                ColumnTypeName::Text => ColumnTypeName::TextArray,
17753                ColumnTypeName::Int => ColumnTypeName::IntArray,
17754                ColumnTypeName::BigInt => ColumnTypeName::BigIntArray,
17755                // v7.40.0 — `oid[]`. Everything downstream of the
17756                // parser already handled `DataType::OidArray`; this
17757                // arm is the whole of what was missing.
17758                ColumnTypeName::Oid => ColumnTypeName::OidArray,
17759                // v7.37.5 β-P4 — INTERVAL[] via the same postfix
17760                // `[]` grammar. Wire OID 1187.
17761                ColumnTypeName::Interval => ColumnTypeName::IntervalArray,
17762                // v7.37.5 γ — full PG array-of-scalar family.
17763                ColumnTypeName::Bool => ColumnTypeName::BoolArray,
17764                ColumnTypeName::SmallInt => ColumnTypeName::SmallIntArray,
17765                ColumnTypeName::Float => ColumnTypeName::FloatArray,
17766                // NUMERIC(p, s) loses its precision params at the
17767                // array level (matches PG: `NUMERIC[]` is untyped,
17768                // per-element precision flows through values).
17769                ColumnTypeName::Numeric(_, _) => ColumnTypeName::NumericArray,
17770                ColumnTypeName::Date => ColumnTypeName::DateArray,
17771                ColumnTypeName::Timestamp => ColumnTypeName::TimestampArray,
17772                ColumnTypeName::Timestamptz => ColumnTypeName::TimestamptzArray,
17773                ColumnTypeName::Uuid => ColumnTypeName::UuidArray,
17774                ColumnTypeName::Json => ColumnTypeName::JsonArray,
17775                ColumnTypeName::Jsonb => ColumnTypeName::JsonbArray,
17776                ColumnTypeName::Bytes => ColumnTypeName::BytesArray,
17777                // VARCHAR(n)[] / CHAR(n)[] drop the length cap at
17778                // the array level (matches PG semantics where the
17779                // element precision is per-row, not column-wide).
17780                ColumnTypeName::Varchar(_) => ColumnTypeName::VarcharArray,
17781                ColumnTypeName::Char(_) => ColumnTypeName::CharArray,
17782                // v7.40.0 — TIME(p)[] / TIMETZ(p)[] drop the
17783                // precision the same way NUMERIC[] does.
17784                ColumnTypeName::Real => ColumnTypeName::RealArray,
17785                ColumnTypeName::Time => ColumnTypeName::TimeArray,
17786                ColumnTypeName::TimeTz => ColumnTypeName::TimeTzArray,
17787                ColumnTypeName::Inet => ColumnTypeName::InetArray,
17788                ColumnTypeName::Xml => ColumnTypeName::XmlArray,
17789                // v7.37.5 ζ-A — MONEY[] (OID 791) ship-triage
17790                // follow-up.
17791                ColumnTypeName::Money => ColumnTypeName::MoneyArray,
17792                other => {
17793                    return Err(self.err(alloc::format!("{other:?}[] not yet supported")));
17794                }
17795            };
17796            // v7.17.0 Phase 3.P0-40 — second `[]` widens 1D → 2D
17797            // for INT/TEXT/BIGINT. Anything else is an error.
17798            if matches!(self.peek(), Token::LBracket) {
17799                self.advance();
17800                if !matches!(self.peek(), Token::RBracket) {
17801                    return Err(self.err(alloc::format!(
17802                        "TYPE[][] second dimension takes no size; got {:?}",
17803                        self.peek()
17804                    )));
17805                }
17806                self.advance();
17807                ty = match ty {
17808                    ColumnTypeName::IntArray => ColumnTypeName::IntArray2D,
17809                    ColumnTypeName::BigIntArray => ColumnTypeName::BigIntArray2D,
17810                    ColumnTypeName::TextArray => ColumnTypeName::TextArray2D,
17811                    // v7.39 (read01 round 75) — bool[][].
17812                    ColumnTypeName::BoolArray => ColumnTypeName::BoolArray2D,
17813                    other => {
17814                        return Err(self.err(alloc::format!(
17815                            "v7.17 2D arrays support INT[][] / BIGINT[][] / \
17816                             TEXT[][] only; got {other:?}"
17817                        )));
17818                    }
17819                };
17820            }
17821        }
17822        Ok((
17823            ty,
17824            implied_auto_increment,
17825            implied_not_null,
17826            user_type_ref,
17827            collation,
17828            collation_explicit,
17829            collation_name,
17830            is_unsigned,
17831            inline_enum_variants,
17832            inline_set_variants,
17833            mysql_int_width,
17834            mysql_fsp,
17835            mysql_declared_timestamp,
17836            mysql_float_md,
17837        ))
17838    }
17839
17840    fn parse_column_def(&mut self) -> Result<ColumnDef, ParseError> {
17841        // v7.20 — PG reserves the table-constraint keywords, so a
17842        // BARE `UNIQUE` / `PRIMARY` / … in column position is a
17843        // malformed constraint clause (e.g. `UNIQUE a` missing its
17844        // parens), not a column named "unique". Since v7.17's
17845        // unknown-type leniency (`user_type_ref`) such a clause
17846        // would otherwise parse as a column with a user-defined
17847        // type — silently accepting invalid DDL. Quoted
17848        // identifiers ("unique" / `unique`) remain valid names.
17849        if let Token::Ident(s) = self.peek()
17850            && [
17851                "unique",
17852                "primary",
17853                "foreign",
17854                "constraint",
17855                "check",
17856                "references",
17857                "exclude",
17858            ]
17859            .iter()
17860            .any(|kw| s.eq_ignore_ascii_case(kw))
17861        {
17862            return Err(self.err(alloc::format!(
17863                "unexpected reserved keyword '{s}' at start of column definition \
17864                 (malformed table constraint?)"
17865            )));
17866        }
17867        let name_tok = self.pos;
17868        let name = self.expect_ident_like()?;
17869        // v7.39.3 — MySQL 9.7.2 reports a column by the SPELLING it was
17870        // declared with: `MyCol` stays `MyCol` in SHOW COLUMNS, in
17871        // information_schema, and in SHOW CREATE (measured). SPG folded
17872        // an unquoted name, so a table restored from a dump reported
17873        // names the application had never written.
17874        //
17875        // The written form comes back from the source span, which only
17876        // the MySQL dialect keeps. The span runs to the START of the
17877        // next token, so a comment or unusual spacing between them
17878        // arrives with it — hence the check that what came back is the
17879        // same identifier. It is not decoration: without it,
17880        // `CREATE TABLE t (MyCol /* c */ INT)` names the column
17881        // `MyCol /* c */`.
17882        let name = self
17883            .source_span(name_tok, name_tok)
17884            .map(|raw| raw.trim().trim_matches('`').trim_matches('"'))
17885            .filter(|raw| raw.eq_ignore_ascii_case(&name))
17886            .map_or(name, alloc::string::String::from);
17887        let (
17888            ty,
17889            implied_auto_increment,
17890            implied_not_null,
17891            user_type_ref,
17892            collation,
17893            collation_explicit,
17894            collation_name,
17895            is_unsigned,
17896            inline_enum_variants,
17897            inline_set_variants,
17898            mysql_int_width,
17899            mysql_fsp,
17900            mysql_declared_timestamp,
17901            mysql_float_md,
17902        ) = self.parse_type_with_implied_flags()?;
17903        // Column constraints: `DEFAULT <expr>`, `NOT NULL`, and the
17904        // MySQL-flavoured `AUTO_INCREMENT` may appear in any order;
17905        // each at most once.
17906        let mut default: Option<Expr> = None;
17907        let mut nullable = !implied_not_null;
17908        let mut nullability_seen = implied_not_null;
17909        let mut auto_increment = implied_auto_increment;
17910        let mut is_primary_key = false;
17911        let mut is_unique = false;
17912        let mut unique_nulls_not_distinct = false;
17913        let mut constraint_deferrable = false;
17914        let mut constraint_initially_deferred = false;
17915        let mut check: Option<Expr> = None;
17916        let mut on_update_runtime: Option<Expr> = None;
17917        let mut generated_stored_expr: Option<Box<Expr>> = None;
17918        let mut identity_always = false;
17919        loop {
17920            // v7.22 (mailrs round-13 gap 3) — PG 18 catalogs
17921            // not-null constraints by name and pg_dump emits them
17922            // inline: `id bigint CONSTRAINT contacts_id_not_null1
17923            // NOT NULL`. Accept and discard the name; whatever
17924            // constraint follows is parsed by the arms below.
17925            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("constraint")) {
17926                // v7.39 (round 308, V29) — a name on an inline
17927                // REFERENCES belongs to the FOREIGN KEY, and the caller
17928                // (`parse_column_def_with_fk`) is what builds it, so
17929                // leave the whole clause for it. Dropping the name here
17930                // is what made `CONSTRAINT fk_a REFERENCES …` come back
17931                // as the synthesised `c_pid_fkey` — which then could
17932                // not be matched by `SET CONSTRAINTS fk_a`. Peek only:
17933                // `advance()` takes tokens by `mem::replace`, so there
17934                // is no rewinding once consumed.
17935                if matches!(
17936                    self.tokens.get(self.pos + 2),
17937                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("references")
17938                ) {
17939                    break;
17940                }
17941                self.advance();
17942                let _name = self.expect_ident_like()?;
17943                continue;
17944            }
17945            // v7.39 (round 379) — MySQL's SHORT generated-column form
17946            // omits `GENERATED ALWAYS`: `<col> <type> AS (<expr>)
17947            // [STORED | VIRTUAL]`. mysqldump emits the long form (handled
17948            // below), but hand-written schemas and app migrations use this.
17949            // STORED / VIRTUAL is optional (MySQL defaults to VIRTUAL);
17950            // SPG computes-and-stores either way, like the long form.
17951            if matches!(self.peek(), Token::As) {
17952                self.advance();
17953                if !matches!(self.peek(), Token::LParen) {
17954                    return Err(self.err(alloc::format!(
17955                        "expected '(' after AS in a generated column, got {:?}",
17956                        self.peek()
17957                    )));
17958                }
17959                self.advance();
17960                let expr = self.parse_expr(0)?;
17961                if !matches!(self.peek(), Token::RParen) {
17962                    return Err(self.err(alloc::format!(
17963                        "expected ')' after AS (<expr>), got {:?}",
17964                        self.peek()
17965                    )));
17966                }
17967                self.advance();
17968                if matches!(self.peek(), Token::Ident(s)
17969                    if s.eq_ignore_ascii_case("stored") || s.eq_ignore_ascii_case("virtual"))
17970                {
17971                    self.advance();
17972                }
17973                generated_stored_expr = Some(alloc::boxed::Box::new(expr));
17974                continue;
17975            }
17976            // v7.22 (round-13 T2) — inline `GENERATED { ALWAYS |
17977            // BY DEFAULT } AS IDENTITY [(seq options)]` (PG 10+;
17978            // the modern replacement for SERIAL in hand-written
17979            // schemas). Both flavours map onto the auto-increment
17980            // machinery — SPG's serial semantics ≈ BY DEFAULT;
17981            // ALWAYS's reject-explicit-values nuance is documented
17982            // leniency. Generated EXPRESSION columns
17983            // (`AS (expr) STORED`) are not supported: error loudly
17984            // instead of silently storing NULLs.
17985            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("generated")) {
17986                self.advance();
17987                let mut saw_generated_always = false;
17988                match self.peek().clone() {
17989                    Token::Ident(s) if s.eq_ignore_ascii_case("always") => {
17990                        self.advance();
17991                        saw_generated_always = true;
17992                    }
17993                    Token::Ident(b) | Token::QuotedIdent(b) if b.eq_ignore_ascii_case("by") => {
17994                        self.advance();
17995                        if !matches!(self.peek(), Token::Default) {
17996                            return Err(self.err(alloc::format!(
17997                                "expected DEFAULT after GENERATED BY, got {:?}",
17998                                self.peek()
17999                            )));
18000                        }
18001                        self.advance();
18002                    }
18003                    other => {
18004                        return Err(self.err(alloc::format!(
18005                            "expected ALWAYS or BY DEFAULT after GENERATED, got {other:?}"
18006                        )));
18007                    }
18008                }
18009                if !matches!(self.peek(), Token::As) {
18010                    return Err(self.err(alloc::format!(
18011                        "expected AS after GENERATED ALWAYS/BY DEFAULT, got {:?}",
18012                        self.peek()
18013                    )));
18014                }
18015                self.advance();
18016                // v7.37.7(sentori Epic 3 P1)— `GENERATED ALWAYS AS
18017                // ( <expr> ) STORED` stored computed-column. The
18018                // expression is captured for the engine to recompute
18019                // on every INSERT / UPDATE. v7.37.7 accepts the
18020                // STORED keyword only; PG also has VIRTUAL, which
18021                // v7.37.7 carves out (sentori only uses STORED).
18022                if matches!(self.peek(), Token::LParen) {
18023                    self.advance();
18024                    let expr = self.parse_expr(0)?;
18025                    if !matches!(self.peek(), Token::RParen) {
18026                        return Err(self.err(alloc::format!(
18027                            "expected ')' after GENERATED ALWAYS AS (<expr>), got {:?}",
18028                            self.peek()
18029                        )));
18030                    }
18031                    self.advance();
18032                    let stored = match self.peek() {
18033                        Token::Ident(s) | Token::QuotedIdent(s)
18034                            if s.eq_ignore_ascii_case("stored") =>
18035                        {
18036                            self.advance();
18037                            true
18038                        }
18039                        // v7.38 (read01 P4.14) — accept PG 18's VIRTUAL
18040                        // generated columns. SPG computes them on write and
18041                        // persists like STORED; the two are observably
18042                        // identical for query results (the value, recompute
18043                        // on base-column change, and NOT NULL enforcement all
18044                        // match), so a PG 18 schema/dump using VIRTUAL loads
18045                        // and behaves correctly. The compute-on-read storage
18046                        // saving is an invisible internal difference.
18047                        Token::Ident(s) | Token::QuotedIdent(s)
18048                            if s.eq_ignore_ascii_case("virtual") =>
18049                        {
18050                            self.advance();
18051                            false
18052                        }
18053                        other => {
18054                            return Err(self.err(alloc::format!(
18055                                "expected STORED or VIRTUAL after GENERATED ALWAYS AS (<expr>), \
18056                                 got {other:?}"
18057                            )));
18058                        }
18059                    };
18060                    let _ = stored; // STORED / VIRTUAL both compute-and-store.
18061                    generated_stored_expr = Some(Box::new(expr));
18062                    continue;
18063                }
18064                self.expect_keyword_ident("identity")?;
18065                // Optional `(START WITH 1 INCREMENT BY 1 …)` —
18066                // consume the balanced parens and discard (SPG's
18067                // auto-increment is max+1-scan based).
18068                if matches!(self.peek(), Token::LParen) {
18069                    let mut depth = 0usize;
18070                    loop {
18071                        match self.advance() {
18072                            Token::LParen => depth += 1,
18073                            Token::RParen => {
18074                                depth -= 1;
18075                                if depth == 0 {
18076                                    break;
18077                                }
18078                            }
18079                            Token::Eof => {
18080                                return Err(self.err(
18081                                    "unterminated sequence-options parens after IDENTITY".into(),
18082                                ));
18083                            }
18084                            _ => {}
18085                        }
18086                    }
18087                }
18088                auto_increment = true;
18089                // v7.38 (read01) — remember the ALWAYS flavour so the engine
18090                // can reject explicit non-DEFAULT INSERT values (unless
18091                // OVERRIDING SYSTEM VALUE) the way PG does.
18092                identity_always = saw_generated_always;
18093                // PG identity columns are implicitly NOT NULL.
18094                nullable = false;
18095                continue;
18096            }
18097            // v7.17.0 Phase 2.1 — MySQL `ON UPDATE
18098            // CURRENT_TIMESTAMP[(N)]`. Only CURRENT_TIMESTAMP
18099            // is accepted today. The "ON" token is an Ident
18100            // (not reserved) — peek before consuming.
18101            if matches!(self.peek(), Token::On)
18102                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("update"))
18103            {
18104                self.advance(); // ON
18105                self.advance(); // update
18106                // Accept CURRENT_TIMESTAMP / CURRENT_TIMESTAMP(N).
18107                let next = self.peek().clone();
18108                match next {
18109                    Token::Ident(s) | Token::QuotedIdent(s)
18110                        if s.eq_ignore_ascii_case("current_timestamp") =>
18111                    {
18112                        self.advance();
18113                        // Optional `(N)` precision.
18114                        if matches!(self.peek(), Token::LParen) {
18115                            self.advance();
18116                            if !matches!(self.peek(), Token::Integer(_)) {
18117                                return Err(self.err(alloc::format!(
18118                                    "expected integer precision inside CURRENT_TIMESTAMP(…), got {:?}",
18119                                    self.peek()
18120                                )));
18121                            }
18122                            self.advance();
18123                            if !matches!(self.peek(), Token::RParen) {
18124                                return Err(self.err(alloc::format!(
18125                                    "expected ')' after CURRENT_TIMESTAMP precision, got {:?}",
18126                                    self.peek()
18127                                )));
18128                            }
18129                            self.advance();
18130                        }
18131                        on_update_runtime = Some(Expr::FunctionCall {
18132                            name: "now".into(),
18133                            args: Vec::new(),
18134                        });
18135                        continue;
18136                    }
18137                    other => {
18138                        return Err(self.err(alloc::format!(
18139                            "v7.17 only supports ON UPDATE CURRENT_TIMESTAMP, got {other:?}"
18140                        )));
18141                    }
18142                }
18143            }
18144            if matches!(self.peek(), Token::Default) {
18145                if default.is_some() {
18146                    return Err(self.err("DEFAULT specified twice".into()));
18147                }
18148                self.advance();
18149                default = Some(self.parse_expr(0)?);
18150                continue;
18151            }
18152            // v7.39 (round 621) — `NOT DEFERRABLE` shares this arm's leading
18153            // token with NOT NULL and sits EARLIER in the loop than the
18154            // deferrability arm, so without the lookahead it was reported as
18155            // "NOT NULL specified twice" (or "expected NULL after NOT").
18156            if matches!(self.peek(), Token::Not)
18157                && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable"))
18158            {
18159                // NOT DEFERRABLE — explicit immediate; nothing to carry.
18160                self.consume_optional_deferrable_clauses()?;
18161                continue;
18162            }
18163            if matches!(self.peek(), Token::Not) {
18164                if nullability_seen {
18165                    return Err(self.err("NOT NULL specified twice".into()));
18166                }
18167                self.advance();
18168                if !matches!(self.peek(), Token::Null) {
18169                    return Err(self.err(format!(
18170                        "expected NULL after NOT in column def, got {:?}",
18171                        self.peek()
18172                    )));
18173                }
18174                self.advance();
18175                nullable = false;
18176                nullability_seen = true;
18177                continue;
18178            }
18179            // v7.14.0 — MySQL accepts a bare `NULL` as an explicit
18180            // "this column is nullable" marker (the default in
18181            // standard SQL anyway). mysqldump emits it routinely
18182            // (`col TYPE NULL DEFAULT NULL` for nullable
18183            // timestamps etc). Accept + no-op.
18184            if matches!(self.peek(), Token::Null) {
18185                if nullability_seen && !nullable {
18186                    // v7.39 (round 761, F31 tranche 2 #31) — PG's
18187                    // sentence, PG18-measured (the table name is the
18188                    // caller's; the column half is exact).
18189                    return Err(self.err(alloc::format!(
18190                        "conflicting NULL/NOT NULL declarations for column \"{name}\""
18191                    )));
18192                }
18193                self.advance();
18194                nullable = true;
18195                nullability_seen = true;
18196                continue;
18197            }
18198            // `AUTO_INCREMENT` or its abbreviated form `AUTOINCREMENT`
18199            // arrives as a bare Ident. Match either, case-insensitive.
18200            if let Token::Ident(s) = self.peek()
18201                && (s.eq_ignore_ascii_case("auto_increment")
18202                    || s.eq_ignore_ascii_case("autoincrement"))
18203            {
18204                if auto_increment {
18205                    return Err(self.err("AUTO_INCREMENT specified twice".into()));
18206                }
18207                self.advance();
18208                auto_increment = true;
18209                continue;
18210            }
18211            // v7.9.13 — inline `PRIMARY KEY` column constraint
18212            // (mailrs F1). Implies `NOT NULL`. The engine creates
18213            // a BTree index for the PK column at CREATE TABLE time
18214            // so FK parent-side index lookups resolve.
18215            // v7.39 (round 621) — `[NOT] DEFERRABLE [INITIALLY {DEFERRED |
18216            // IMMEDIATE}]` after an inline PK / UNIQUE / REFERENCES. Every
18217            // spelling was a parse error, so a pg_dump carrying one stopped
18218            // mid-restore. The clauses are consumed by the same helper the FK
18219            // path has used since round 288 and recorded nowhere: SPG enforces
18220            // the constraint IMMEDIATELY either way, which fails earlier than
18221            // PG inside a transaction that violates-then-repairs — a refusal,
18222            // not a wrong answer. True deferral is the open remainder of F08.
18223            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("deferrable") || s.eq_ignore_ascii_case("initially"))
18224                || (matches!(self.peek(), Token::Not)
18225                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s)) if s.eq_ignore_ascii_case("deferrable")))
18226            {
18227                // v7.39 (round 711) — CARRIED now (the storing half of
18228                // F08); round 621 only consumed.
18229                let (d, idef) = self.consume_deferrable_clauses_timed()?;
18230                constraint_deferrable |= d;
18231                constraint_initially_deferred |= idef;
18232                continue;
18233            }
18234            if let Token::Ident(s) = self.peek()
18235                && s.eq_ignore_ascii_case("primary")
18236            {
18237                if is_primary_key {
18238                    return Err(self.err("PRIMARY KEY specified twice".into()));
18239                }
18240                // Peek-ahead for the required `KEY` token.
18241                let next = self.tokens.get(self.pos + 1);
18242                let next_is_key = matches!(
18243                    next,
18244                    Some(Token::Ident(k)) if k.eq_ignore_ascii_case("key")
18245                );
18246                if !next_is_key {
18247                    return Err(self.err(format!(
18248                        "expected KEY after PRIMARY in column def, got {:?}",
18249                        next
18250                    )));
18251                }
18252                self.advance(); // PRIMARY
18253                self.advance(); // KEY
18254                is_primary_key = true;
18255                if nullability_seen && nullable {
18256                    return Err(self.err(
18257                        "column declared NULL but inline PRIMARY KEY implies NOT NULL".into(),
18258                    ));
18259                }
18260                nullable = false;
18261                nullability_seen = true;
18262                continue;
18263            }
18264            // v7.13.0 — inline `UNIQUE` column constraint
18265            // (mailrs round-5 G2). Fold into a single-column
18266            // table-level UNIQUE at CREATE TABLE post-process time.
18267            if let Token::Ident(s) = self.peek()
18268                && s.eq_ignore_ascii_case("unique")
18269            {
18270                if is_unique {
18271                    return Err(self.err("UNIQUE specified twice".into()));
18272                }
18273                self.advance();
18274                is_unique = true;
18275                // v7.38 (read01 P4.19) — optional `NULLS [NOT] DISTINCT`
18276                // (PG 15+); default is NULLS DISTINCT per the SQL standard.
18277                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nulls")) {
18278                    let n1 = self.tokens.get(self.pos + 1);
18279                    let n2 = self.tokens.get(self.pos + 2);
18280                    if matches!(n1, Some(Token::Not)) && matches!(n2, Some(Token::Distinct)) {
18281                        self.advance(); // NULLS
18282                        self.advance(); // NOT
18283                        self.advance(); // DISTINCT
18284                        unique_nulls_not_distinct = true;
18285                    } else if matches!(n1, Some(Token::Distinct)) {
18286                        self.advance(); // NULLS
18287                        self.advance(); // DISTINCT
18288                    }
18289                }
18290                continue;
18291            }
18292            // v7.13.0 — inline `CHECK (<expr>)` column constraint
18293            // (mailrs round-5 G3). PG semantics: column-level
18294            // CHECK is equivalent to a table-level CHECK. Multiple
18295            // inline CHECKs on the same column AND together.
18296            if let Token::Ident(s) = self.peek()
18297                && s.eq_ignore_ascii_case("check")
18298            {
18299                self.advance();
18300                if !matches!(self.peek(), Token::LParen) {
18301                    return Err(self.err(alloc::format!(
18302                        "expected '(' after CHECK in column def, got {:?}",
18303                        self.peek()
18304                    )));
18305                }
18306                self.advance();
18307                let pred = self.parse_expr(0)?;
18308                if !matches!(self.peek(), Token::RParen) {
18309                    return Err(self.err(alloc::format!(
18310                        "expected ')' to close CHECK predicate, got {:?}",
18311                        self.peek()
18312                    )));
18313                }
18314                self.advance();
18315                check = Some(match check.take() {
18316                    Some(prev) => Expr::Binary {
18317                        op: BinOp::And,
18318                        lhs: Box::new(prev),
18319                        rhs: Box::new(pred),
18320                    },
18321                    None => pred,
18322                });
18323                continue;
18324            }
18325            break;
18326        }
18327        Ok(ColumnDef {
18328            name,
18329            ty,
18330            nullable,
18331            default,
18332            auto_increment,
18333            is_primary_key,
18334            is_unique,
18335            unique_nulls_not_distinct,
18336            constraint_deferrable,
18337            constraint_initially_deferred,
18338            check,
18339            user_type_ref,
18340            on_update_runtime,
18341            collation,
18342            collation_explicit,
18343            collation_name,
18344            is_unsigned,
18345            inline_enum_variants,
18346            inline_set_variants,
18347            generated_stored_expr,
18348            identity_always,
18349            mysql_int_width,
18350            mysql_fsp,
18351            mysql_declared_timestamp,
18352            mysql_float_md,
18353        })
18354    }
18355
18356    /// `NUMERIC` may appear without parameters, with one (precision
18357    /// only, scale=0), or with both. Returns `(precision, scale)` with
18358    /// 0 = unspecified for the bare form.
18359    fn parse_optional_numeric_params(&mut self) -> Result<(u16, i16), ParseError> {
18360        if !matches!(self.peek(), Token::LParen) {
18361            // Bare `NUMERIC` — PG treats this as "unlimited precision";
18362            // we surface it as precision=0 to mean "unconstrained" so
18363            // the engine doesn't need a separate variant.
18364            return Ok((0, 0));
18365        }
18366        self.advance();
18367        // v7.39 (round 272) — PG's declared precision runs to 1000, and
18368        // it words the out-of-range case with the value it saw. SPG
18369        // capped at 38 (i128's width), so a `numeric(50,10)` column PG
18370        // accepts failed to parse at all; values wider than i128 are
18371        // carried by the arbitrary-precision form.
18372        let precision = match self.advance() {
18373            Token::Integer(n) if (1..=1000).contains(&n) => {
18374                u16::try_from(n).expect("range-checked")
18375            }
18376            Token::Integer(n) => {
18377                return Err(ParseError {
18378                    message: format!("NUMERIC precision {n} must be between 1 and 1000"),
18379                    token_pos: self.consumed_pos(),
18380                });
18381            }
18382            other => {
18383                return Err(ParseError {
18384                    message: format!(
18385                        "NUMERIC precision must be an integer in 1..=1000, got {other:?}"
18386                    ),
18387                    token_pos: self.consumed_pos(),
18388                });
18389            }
18390        };
18391        // v7.39 (round 273) — PG's declared scale runs -1000..=1000 and is
18392        // NOT bounded by the precision (`numeric(10,11)` is legal; a value
18393        // then overflows). A negative scale rounds to tens / hundreds / …
18394        let scale = if matches!(self.peek(), Token::Comma) {
18395            self.advance();
18396            let neg = if matches!(self.peek(), Token::Minus) {
18397                self.advance();
18398                true
18399            } else {
18400                false
18401            };
18402            match self.advance() {
18403                Token::Integer(n) => {
18404                    let signed = if neg { -n } else { n };
18405                    if !(-1000..=1000).contains(&signed) {
18406                        return Err(ParseError {
18407                            message: format!(
18408                                "NUMERIC scale {signed} must be between -1000 and 1000"
18409                            ),
18410                            token_pos: self.consumed_pos(),
18411                        });
18412                    }
18413                    i16::try_from(signed).expect("range-checked")
18414                }
18415                other => {
18416                    return Err(ParseError {
18417                        message: format!("NUMERIC scale must be an integer, got {other:?}"),
18418                        token_pos: self.consumed_pos(),
18419                    });
18420                }
18421            }
18422        } else {
18423            0
18424        };
18425        if !matches!(self.peek(), Token::RParen) {
18426            return Err(self.err(format!(
18427                "expected ')' to close NUMERIC params, got {:?}",
18428                self.peek()
18429            )));
18430        }
18431        self.advance();
18432        Ok((precision, scale))
18433    }
18434
18435    /// Parse `(N)` where `N` is a positive integer literal — used by the
18436    /// `VARCHAR`/`CHAR`/`VECTOR` column types. `label` is the type name
18437    /// for the error message.
18438    /// v6.0.1: parse the optional `USING <encoding>` clause that
18439    /// follows `VECTOR(N)` in a column definition. Missing clause
18440    /// → `VecEncoding::F32` (pre-v6 default). Unknown encoding
18441    /// ident → `ParseError` listing the encodings recognised today.
18442    fn parse_optional_vector_encoding(&mut self) -> Result<VecEncoding, ParseError> {
18443        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
18444            return Ok(VecEncoding::F32);
18445        }
18446        // v7.13.2 — mailrs round-6 S6: `USING` after a vector type
18447        // overlaps with `ALTER COLUMN TYPE … USING <expr>`. Only
18448        // consume the token when the very next token is a known
18449        // vector-encoding keyword (SQ8 / HALF). Otherwise leave
18450        // `USING` for the caller — it's the rewrite-expression form.
18451        let n1 = self.tokens.get(self.pos + 1);
18452        let next_is_encoding = matches!(
18453            n1,
18454            Some(Token::Ident(s))
18455                if s.eq_ignore_ascii_case("sq8") || s.eq_ignore_ascii_case("half")
18456        );
18457        if !next_is_encoding {
18458            return Ok(VecEncoding::F32);
18459        }
18460        self.advance();
18461        let enc_ident = match self.advance() {
18462            Token::Ident(s) => s,
18463            other => {
18464                return Err(self.err(format!(
18465                    "expected vector encoding after USING, got {other:?}"
18466                )));
18467            }
18468        };
18469        match enc_ident.to_ascii_lowercase().as_str() {
18470            "sq8" => Ok(VecEncoding::Sq8),
18471            // v6.0.3: `HALF` (pgvector convention) selects IEEE-754
18472            // binary16 per-element storage.
18473            "half" => Ok(VecEncoding::F16),
18474            other => Err(self.err(format!(
18475                "unknown vector encoding {other:?}; supported: SQ8, HALF"
18476            ))),
18477        }
18478    }
18479
18480    /// v7.17.0 Phase 4.3 — peek at the MySQL display-width
18481    /// without consuming it. Returns `Some(N)` when the next
18482    /// tokens are `( <int> )`; None otherwise. Used by the
18483    /// TINYINT classifier to decide whether to map to Bool or
18484    /// SmallInt.
18485    fn peek_optional_paren_size_value(&self) -> Option<i64> {
18486        if !matches!(self.peek(), Token::LParen) {
18487            return None;
18488        }
18489        let next = self.tokens.get(self.pos + 1)?;
18490        let n = match next {
18491            Token::Integer(n) => *n,
18492            _ => return None,
18493        };
18494        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18495            return None;
18496        }
18497        Some(n)
18498    }
18499
18500    /// v7.14.0 — consume an optional MySQL display-width
18501    /// parenthesised number after an integer type, returning
18502    /// nothing. `TINYINT(1)` etc.
18503    /// v7.39 (round 360) — does the parenthesised group ahead contain a
18504    /// comma, i.e. is it MySQL's `(m,d)` rather than PG's `(p)`?
18505    fn peek_paren_has_comma(&self) -> bool {
18506        let mut i = self.pos + 1;
18507        let mut depth = 1usize;
18508        while depth > 0 {
18509            match self.tokens.get(i) {
18510                Some(Token::LParen) => depth += 1,
18511                Some(Token::RParen) => depth -= 1,
18512                Some(Token::Comma) if depth == 1 => return true,
18513                None | Some(Token::Eof) => return false,
18514                _ => {}
18515            }
18516            i += 1;
18517        }
18518        false
18519    }
18520
18521    /// v7.39 (round 424) — the same optional `(N)` modifier, but RETURNING
18522    /// the number. Temporal columns need it: MySQL's `DATETIME(3)` declares a
18523    /// fractional-seconds precision that drives write truncation and render
18524    /// padding, where `consume_optional_paren_size` throws it away.
18525    /// `Some(0)` for an explicit `(0)`, `None` when no modifier is written.
18526    fn take_optional_paren_size(&mut self) -> Option<u8> {
18527        let Some(Token::Integer(n)) = self
18528            .tokens
18529            .get(self.pos + 1)
18530            .filter(|_| matches!(self.peek(), Token::LParen))
18531            .cloned()
18532        else {
18533            self.consume_optional_paren_size();
18534            return None;
18535        };
18536        if !matches!(self.tokens.get(self.pos + 2), Some(Token::RParen)) {
18537            self.consume_optional_paren_size();
18538            return None;
18539        }
18540        self.consume_optional_paren_size();
18541        u8::try_from(n).ok()
18542    }
18543
18544    fn consume_optional_paren_size(&mut self) {
18545        if !matches!(self.peek(), Token::LParen) {
18546            return;
18547        }
18548        self.advance();
18549        // Skip until matching RParen (allow nested or any tokens).
18550        let mut depth = 1usize;
18551        while depth > 0 {
18552            match self.peek() {
18553                Token::LParen => depth += 1,
18554                Token::RParen => depth -= 1,
18555                Token::Eof => return,
18556                _ => {}
18557            }
18558            self.advance();
18559        }
18560    }
18561
18562    fn parse_paren_size(&mut self, label: &str) -> Result<u32, ParseError> {
18563        if !matches!(self.peek(), Token::LParen) {
18564            return Err(self.err(format!("{label} type requires (N), got {:?}", self.peek())));
18565        }
18566        self.advance();
18567        let n = match self.advance() {
18568            Token::Integer(n) if n > 0 => u32::try_from(n).map_err(|_| ParseError {
18569                message: format!("{label} size too large: {n}"),
18570                token_pos: self.consumed_pos(),
18571            })?,
18572            other => {
18573                return Err(ParseError {
18574                    message: format!("expected positive integer {label} size, got {other:?}"),
18575                    token_pos: self.consumed_pos(),
18576                });
18577            }
18578        };
18579        if !matches!(self.peek(), Token::RParen) {
18580            return Err(self.err(format!(
18581                "expected ')' after {label} size, got {:?}",
18582                self.peek()
18583            )));
18584        }
18585        self.advance();
18586        Ok(n)
18587    }
18588
18589    /// v7.39 (round 406) — the `ON CONFLICT DO NOTHING` clause that MySQL's
18590    /// `INSERT IGNORE` lowers to: a bare target (arbitrate on every unique
18591    /// key, like MySQL) whose action skips conflicting rows.
18592    /// v7.39 (round 419) — resolve the conflict clause for ANY of the four
18593    /// INSERT source forms (VALUES / SELECT / parenthesized source / WITH).
18594    /// Before this the MySQL upsert lowerings (`ON DUPLICATE KEY UPDATE`,
18595    /// `REPLACE INTO`) were wired into the VALUES branch ONLY, so the very
18596    /// common bulk-upsert spellings —
18597    ///     INSERT INTO t SELECT … ON DUPLICATE KEY UPDATE c = VALUES(c)
18598    ///     REPLACE INTO t SELECT …
18599    /// — were a parse error / a duplicate-key failure respectively.
18600    ///
18601    /// Precedence: an explicitly written clause beats a statement-level flag.
18602    /// `ON DUPLICATE KEY UPDATE` first, then PG's own `ON CONFLICT`, then the
18603    /// implicit `REPLACE` and `IGNORE` lowerings.
18604    fn parse_insert_conflict_clause(
18605        &mut self,
18606        replace: bool,
18607        ignore: bool,
18608    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18609        if let Some(c) = self.parse_optional_on_duplicate_key()? {
18610            return Ok(Some(c));
18611        }
18612        if let Some(c) = self.parse_optional_on_conflict()? {
18613            return Ok(Some(c));
18614        }
18615        if replace {
18616            // REPLACE INTO = delete-then-insert, which PG spells as
18617            // `ON CONFLICT DO UPDATE SET` over every column; the engine
18618            // reads an empty assignment list as "take the incoming row".
18619            return Ok(Some(crate::ast::OnConflictClause {
18620                target_columns: Vec::new(),
18621                index_where: None,
18622                constraint_name: None,
18623                mysql_lowered: true,
18624                action: crate::ast::OnConflictAction::Update {
18625                    assignments: Vec::new(),
18626                    where_: None,
18627                },
18628            }));
18629        }
18630        if ignore {
18631            return Ok(Some(Self::insert_ignore_clause()));
18632        }
18633        Ok(None)
18634    }
18635
18636    /// v7.39 (round 419, extracted from the VALUES branch) — MySQL's
18637    /// `ON DUPLICATE KEY UPDATE col = expr [, …]`. Bare target (MySQL
18638    /// watches every unique key, which `mysql_lowered` records); `VALUES(col)`
18639    /// in an assignment is MySQL's spelling of `EXCLUDED.col`.
18640    fn parse_optional_on_duplicate_key(
18641        &mut self,
18642    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
18643        if !(matches!(self.peek(), Token::On)
18644            && matches!(self.tokens.get(self.pos + 1),
18645                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("duplicate")))
18646        {
18647            return Ok(None);
18648        }
18649        self.advance(); // ON
18650        self.advance(); // DUPLICATE
18651        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("key")) {
18652            return Err(self.err(format!(
18653                "expected KEY after ON DUPLICATE, got {:?}",
18654                self.peek()
18655            )));
18656        }
18657        self.advance();
18658        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("update")) {
18659            return Err(self.err(format!(
18660                "expected UPDATE after ON DUPLICATE KEY, got {:?}",
18661                self.peek()
18662            )));
18663        }
18664        self.advance();
18665        let mut assignments: Vec<(String, Expr)> = Vec::new();
18666        loop {
18667            let col = self.expect_ident_like()?;
18668            if !matches!(self.peek(), Token::Eq) {
18669                return Err(self.err(format!(
18670                    "expected '=' in ON DUPLICATE KEY UPDATE, got {:?}",
18671                    self.peek()
18672                )));
18673            }
18674            self.advance();
18675            let mut expr = self.parse_expr(0)?;
18676            Self::rewrite_mysql_values_refs(&mut expr);
18677            assignments.push((col, expr));
18678            if matches!(self.peek(), Token::Comma) {
18679                self.advance();
18680                continue;
18681            }
18682            break;
18683        }
18684        Ok(Some(crate::ast::OnConflictClause {
18685            target_columns: Vec::new(),
18686            index_where: None,
18687            constraint_name: None,
18688            mysql_lowered: true,
18689            action: crate::ast::OnConflictAction::Update {
18690                assignments,
18691                where_: None,
18692            },
18693        }))
18694    }
18695
18696    fn insert_ignore_clause() -> crate::ast::OnConflictClause {
18697        crate::ast::OnConflictClause {
18698            target_columns: Vec::new(),
18699            index_where: None,
18700            constraint_name: None,
18701            mysql_lowered: true,
18702            action: crate::ast::OnConflictAction::Nothing,
18703        }
18704    }
18705
18706    fn parse_insert_stmt(&mut self, replace: bool) -> Result<Statement, ParseError> {
18707        debug_assert!(
18708            matches!(self.peek(), Token::Insert)
18709                || (replace
18710                    && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("replace")))
18711        );
18712        self.advance();
18713        // v7.39 (round 406) — MySQL `INSERT IGNORE INTO t …` skips a row that
18714        // would raise a duplicate-key error instead of failing the statement,
18715        // i.e. `ON CONFLICT DO NOTHING` over every unique key. IGNORE is a
18716        // plain ident to the lexer; only the MySQL dialect accepts it here.
18717        let ignore = self.mysql_dialect
18718            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ignore"));
18719        if ignore {
18720            self.advance();
18721        }
18722        if !matches!(self.peek(), Token::Into) {
18723            return Err(self.err(format!("expected INTO after INSERT, got {:?}", self.peek())));
18724        }
18725        self.advance();
18726        let table = self.expect_ident_like()?;
18727        // v7.39 (round 240) — `INSERT INTO t AS alias`: PG's insert_target
18728        // grammar requires the AS keyword here (a bare identifier would be
18729        // ambiguous with a column list). The alias is what the ON CONFLICT
18730        // DO UPDATE expressions refer to the target row by.
18731        let alias = if matches!(self.peek(), Token::As) {
18732            self.advance();
18733            Some(self.expect_ident_like()?)
18734        } else {
18735            None
18736        };
18737        // v7.39 (round 428) — MySQL's SET-form INSERT:
18738        //     INSERT INTO t SET a = 1, b = 'x'
18739        // It is exactly `INSERT INTO t (a, b) VALUES (1, 'x')` — omitted
18740        // columns take their DEFAULT, `SET a = DEFAULT` is legal, and it
18741        // composes with IGNORE / ON DUPLICATE KEY UPDATE / REPLACE (all
18742        // measured). So it lowers to the column list + one VALUES row and
18743        // rejoins the ordinary path, which already handles every one of
18744        // those. PG has no such spelling, hence the dialect gate.
18745        if self.mysql_dialect
18746            && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set"))
18747        {
18748            self.advance(); // SET
18749            let mut names = Vec::new();
18750            let mut values = Vec::new();
18751            loop {
18752                names.push(self.expect_ident_like()?);
18753                if !matches!(self.peek(), Token::Eq) {
18754                    return Err(self.err(alloc::format!(
18755                        "expected '=' in INSERT … SET, got {:?}",
18756                        self.peek()
18757                    )));
18758                }
18759                self.advance();
18760                // `SET a = DEFAULT` rides the same `__column_default` marker
18761                // the VALUES-row and UPDATE-SET paths use; the INSERT
18762                // executor resolves it against the target column.
18763                if matches!(self.peek(), Token::Default) {
18764                    self.advance();
18765                    values.push(Expr::FunctionCall {
18766                        name: "__column_default".to_string(),
18767                        args: Vec::new(),
18768                    });
18769                } else {
18770                    values.push(self.parse_expr(0)?);
18771                }
18772                if matches!(self.peek(), Token::Comma) {
18773                    self.advance();
18774                    continue;
18775                }
18776                break;
18777            }
18778            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18779            let returning = self.parse_optional_returning()?;
18780            return Ok(Statement::Insert(InsertStatement {
18781                ctes: Vec::new(),
18782                table,
18783                alias,
18784                columns: Some(names),
18785                rows: alloc::vec![values],
18786                select_source: None,
18787                // MySQL's SET form has no `OVERRIDING …` clause (that is
18788                // PG's identity-column spelling).
18789                overriding: Overriding::None,
18790                mysql_ignore: ignore,
18791                on_conflict,
18792                returning,
18793            }));
18794        }
18795        // Optional column list — `INSERT INTO t (a, b) VALUES ...`.
18796        // v7.39 (round 151) — a SELECT or WITH right after the paren is
18797        // a parenthesized query source instead (PG select_with_parens:
18798        // `INSERT INTO t (SELECT …)` / `INSERT INTO t (WITH … SELECT …)`);
18799        // both keywords are reserved in PG, so no column list can start
18800        // with them.
18801        let columns = if matches!(self.peek(), Token::LParen) {
18802            self.advance();
18803            if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18804                let select_stmt = if self.peek_is_with_kw() {
18805                    self.advance();
18806                    self.parse_nested_with_select()?
18807                } else {
18808                    match self.parse_select_stmt()? {
18809                        Statement::Select(s) => s,
18810                        other => {
18811                            return Err(self.err(alloc::format!(
18812                                "expected SELECT in parenthesized INSERT source, got {other:?}"
18813                            )));
18814                        }
18815                    }
18816                };
18817                if !matches!(self.peek(), Token::RParen) {
18818                    return Err(self.err(format!(
18819                        "expected ')' after parenthesized INSERT source, got {:?}",
18820                        self.peek()
18821                    )));
18822                }
18823                self.advance();
18824                let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18825                let returning = self.parse_optional_returning()?;
18826                return Ok(Statement::Insert(InsertStatement {
18827                    ctes: Vec::new(),
18828                    table,
18829                    alias: alias.clone(),
18830                    columns: None,
18831                    rows: Vec::new(),
18832                    select_source: Some(Box::new(select_stmt)),
18833                    on_conflict,
18834                    returning,
18835                    overriding: Overriding::None,
18836                    mysql_ignore: ignore,
18837                }));
18838            }
18839            let mut names = Vec::new();
18840            loop {
18841                names.push(self.expect_ident_like()?);
18842                match self.peek() {
18843                    Token::Comma => {
18844                        self.advance();
18845                    }
18846                    Token::RParen => {
18847                        self.advance();
18848                        break;
18849                    }
18850                    other => {
18851                        return Err(self.err(format!(
18852                            "expected ',' or ')' in INSERT column list, got {other:?}"
18853                        )));
18854                    }
18855                }
18856            }
18857            Some(names)
18858        } else {
18859            None
18860        };
18861        // PG 10+ `OVERRIDING {SYSTEM | USER} VALUE` — pg_dump emits
18862        // OVERRIDING SYSTEM VALUE for its identity columns. The clause
18863        // is captured on the statement so the engine can apply PG's
18864        // GENERATED ALWAYS / BY DEFAULT interaction (v7.38, read01).
18865        let overriding = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overriding"))
18866        {
18867            self.advance();
18868            let which = self.expect_ident_like()?;
18869            let ov = if which.eq_ignore_ascii_case("system") {
18870                Overriding::System
18871            } else if which.eq_ignore_ascii_case("user") {
18872                Overriding::User
18873            } else {
18874                return Err(self.err(format!(
18875                    "expected SYSTEM or USER after OVERRIDING, got {which:?}"
18876                )));
18877            };
18878            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("value")) {
18879                return Err(self.err(format!(
18880                    "expected VALUE after OVERRIDING {}, got {:?}",
18881                    which.to_ascii_uppercase(),
18882                    self.peek()
18883                )));
18884            }
18885            self.advance();
18886            ov
18887        } else {
18888            Overriding::None
18889        };
18890        // `INSERT INTO t DEFAULT VALUES` — a single row made
18891        // entirely of column defaults. Lower to the permuted
18892        // column-list path with an empty list: every schema column
18893        // is unmapped, so the engine fills each from its default
18894        // (serials advance, plain defaults evaluate, the rest NULL).
18895        if matches!(self.peek(), Token::Default) {
18896            self.advance();
18897            if !matches!(self.peek(), Token::Values) {
18898                return Err(self.err(format!(
18899                    "expected VALUES after DEFAULT in INSERT, got {:?}",
18900                    self.peek()
18901                )));
18902            }
18903            self.advance();
18904            if columns.is_some() {
18905                return Err(self.err("DEFAULT VALUES cannot follow an INSERT column list".into()));
18906            }
18907            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18908            let returning = self.parse_optional_returning()?;
18909            return Ok(Statement::Insert(InsertStatement {
18910                ctes: Vec::new(),
18911                table,
18912                alias: alias.clone(),
18913                columns: Some(Vec::new()),
18914                rows: alloc::vec![Vec::new()],
18915                select_source: None,
18916                on_conflict,
18917                returning,
18918                overriding,
18919                mysql_ignore: ignore,
18920            }));
18921        }
18922        // v7.13.0 — `INSERT INTO t [(cols)] SELECT …` (mailrs
18923        // round-5 G4). Dispatch on VALUES vs SELECT. v7.39 (round 151)
18924        // — a WITH-headed source query (`INSERT INTO t WITH c AS (…)
18925        // SELECT …`) heads the SOURCE select, as in PG (the statement's
18926        // own WITH comes before INSERT).
18927        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
18928            let select_stmt = if self.peek_is_with_kw() {
18929                self.advance();
18930                self.parse_nested_with_select()?
18931            } else {
18932                match self.parse_select_stmt()? {
18933                    Statement::Select(s) => s,
18934                    other => {
18935                        return Err(self.err(alloc::format!(
18936                            "expected SELECT after INSERT INTO ... target, got {other:?}"
18937                        )));
18938                    }
18939                }
18940            };
18941            let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
18942            let returning = self.parse_optional_returning()?;
18943            return Ok(Statement::Insert(InsertStatement {
18944                ctes: Vec::new(),
18945                table,
18946                alias: alias.clone(),
18947                columns,
18948                rows: Vec::new(),
18949                select_source: Some(Box::new(select_stmt)),
18950                on_conflict,
18951                returning,
18952                overriding,
18953                mysql_ignore: ignore,
18954            }));
18955        }
18956        if !matches!(self.peek(), Token::Values) {
18957            return Err(self.err(format!(
18958                "expected VALUES or SELECT after table name, got {:?}",
18959                self.peek()
18960            )));
18961        }
18962        self.advance();
18963        if !matches!(self.peek(), Token::LParen) {
18964            return Err(self.err(format!("expected '(' after VALUES, got {:?}", self.peek())));
18965        }
18966        let mut rows = Vec::new();
18967        loop {
18968            // Each iteration consumes one `(expr, expr, …)` tuple.
18969            if !matches!(self.peek(), Token::LParen) {
18970                return Err(self.err(format!(
18971                    "expected '(' for next VALUES tuple, got {:?}",
18972                    self.peek()
18973                )));
18974            }
18975            self.advance();
18976            let mut tuple = Vec::new();
18977            loop {
18978                // v7.38 (read01) — `INSERT INTO t VALUES (…, DEFAULT, …)` uses
18979                // the column's declared default for that slot. Rides out as the
18980                // same `__column_default` marker call the UPDATE `SET c = DEFAULT`
18981                // path uses; the INSERT executor resolves it per target column.
18982                if matches!(self.peek(), Token::Default) {
18983                    self.advance();
18984                    tuple.push(Expr::FunctionCall {
18985                        name: "__column_default".to_string(),
18986                        args: Vec::new(),
18987                    });
18988                } else {
18989                    tuple.push(self.parse_expr(0)?);
18990                }
18991                match self.peek() {
18992                    Token::Comma => {
18993                        self.advance();
18994                    }
18995                    Token::RParen => {
18996                        self.advance();
18997                        break;
18998                    }
18999                    other => {
19000                        return Err(self.err(format!(
19001                            "expected ',' or ')' in VALUES tuple, got {other:?}"
19002                        )));
19003                    }
19004                }
19005            }
19006            if tuple.is_empty() {
19007                return Err(self.err("INSERT VALUES tuple requires at least one value".into()));
19008            }
19009            rows.push(tuple);
19010            // Continue with comma-separated tuples.
19011            if matches!(self.peek(), Token::Comma) {
19012                self.advance();
19013            } else {
19014                break;
19015            }
19016        }
19017        // MySQL `ON DUPLICATE KEY UPDATE col = expr [, …]` — lowers
19018        // to ON CONFLICT DO UPDATE with an empty conflict target
19019        // (the engine picks the table's first unique index, which
19020        // matches MySQL's any-unique-key behaviour for the common
19021        // single-key case). `VALUES(col)` in the assignments is
19022        // MySQL's spelling of EXCLUDED.col.
19023        let on_conflict = self.parse_insert_conflict_clause(replace, ignore)?;
19024        let returning = self.parse_optional_returning()?;
19025        Ok(Statement::Insert(InsertStatement {
19026            ctes: Vec::new(),
19027            table,
19028            alias,
19029            columns,
19030            rows,
19031            select_source: None,
19032            on_conflict,
19033            returning,
19034            overriding,
19035            mysql_ignore: ignore,
19036        }))
19037    }
19038
19039    /// MySQL's `VALUES(col)` inside ON DUPLICATE KEY UPDATE reads
19040    /// the incoming row's value — exactly PG's EXCLUDED.col.
19041    fn rewrite_mysql_values_refs(e: &mut Expr) {
19042        match e {
19043            Expr::FunctionCall { name, args }
19044                if name.eq_ignore_ascii_case("values")
19045                    && args.len() == 1
19046                    && matches!(&args[0], Expr::Column(c) if c.qualifier.is_none()) =>
19047            {
19048                let Expr::Column(c) = &args[0] else {
19049                    unreachable!("guarded above");
19050                };
19051                *e = Expr::Column(crate::ast::ColumnName {
19052                    qualifier: Some("EXCLUDED".to_string()),
19053                    name: c.name.clone(),
19054                });
19055            }
19056            Expr::FunctionCall { args, .. } => {
19057                for a in args {
19058                    Self::rewrite_mysql_values_refs(a);
19059                }
19060            }
19061            Expr::Binary { lhs, rhs, .. } => {
19062                Self::rewrite_mysql_values_refs(lhs);
19063                Self::rewrite_mysql_values_refs(rhs);
19064            }
19065            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
19066                Self::rewrite_mysql_values_refs(expr);
19067            }
19068            Expr::Case {
19069                operand,
19070                branches,
19071                else_branch,
19072            } => {
19073                if let Some(op) = operand {
19074                    Self::rewrite_mysql_values_refs(op);
19075                }
19076                for (w, t) in branches {
19077                    Self::rewrite_mysql_values_refs(w);
19078                    Self::rewrite_mysql_values_refs(t);
19079                }
19080                if let Some(el) = else_branch {
19081                    Self::rewrite_mysql_values_refs(el);
19082                }
19083            }
19084            _ => {}
19085        }
19086    }
19087
19088    /// v7.9.7 — parse the optional `ON CONFLICT (cols) DO …`
19089    /// clause sitting between the INSERT body and the trailing
19090    /// RETURNING. All keywords come in as bare idents; `ON` is
19091    /// a reserved Token though.
19092    fn parse_optional_on_conflict(
19093        &mut self,
19094    ) -> Result<Option<crate::ast::OnConflictClause>, ParseError> {
19095        if !matches!(self.peek(), Token::On) {
19096            return Ok(None);
19097        }
19098        // Peek further: we want exactly "ON CONFLICT ...". If the
19099        // next ident isn't "conflict", let some other parser handle.
19100        let next_is_conflict = matches!(
19101            self.tokens.get(self.pos + 1),
19102            Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("conflict")
19103        );
19104        if !next_is_conflict {
19105            return Ok(None);
19106        }
19107        self.advance(); // ON
19108        self.advance(); // CONFLICT
19109        // v7.37.17 (17.6 siblings) — `ON CONSTRAINT <name>` names
19110        // the constraint instead of listing columns (the pg_dump
19111        // form); the engine resolves it.
19112        let mut constraint_name: Option<String> = None;
19113        if matches!(self.peek(), Token::On) {
19114            self.advance(); // ON
19115            match self.advance() {
19116                Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("constraint") => {
19117                }
19118                other => {
19119                    return Err(self.err(alloc::format!(
19120                        "expected CONSTRAINT after ON CONFLICT ON, got {other:?}"
19121                    )));
19122                }
19123            }
19124            constraint_name = Some(self.expect_ident_like()?);
19125        }
19126        // Optional `(col [, col]*)` target list.
19127        let mut target_columns: Vec<String> = Vec::new();
19128        if matches!(self.peek(), Token::LParen) {
19129            self.advance();
19130            loop {
19131                target_columns.push(self.expect_ident_like()?);
19132                match self.peek() {
19133                    Token::Comma => {
19134                        self.advance();
19135                    }
19136                    Token::RParen => {
19137                        self.advance();
19138                        break;
19139                    }
19140                    other => {
19141                        return Err(self.err(alloc::format!(
19142                            "expected ',' or ')' in ON CONFLICT target list, got {other:?}"
19143                        )));
19144                    }
19145                }
19146            }
19147        }
19148        // v7.39 (round 240) — optional index predicate after the target
19149        // list: `ON CONFLICT (col) WHERE pred DO …`. PG uses it to infer a
19150        // PARTIAL unique index; SPG's arbiters are full indexes, which
19151        // satisfy any predicate, so it is parsed and carried but not
19152        // consulted (recorded residual: partial-unique-index arbiters).
19153        let index_where = if !target_columns.is_empty() && matches!(self.peek(), Token::Where) {
19154            self.advance();
19155            Some(self.parse_expr(0)?)
19156        } else {
19157            None
19158        };
19159        // Required `DO`.
19160        match self.advance() {
19161            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("do") => {}
19162            other => {
19163                return Err(self.err(alloc::format!(
19164                    "expected DO after ON CONFLICT [(…)], got {other:?}"
19165                )));
19166            }
19167        }
19168        // Action: NOTHING | UPDATE SET …
19169        let action = match self.advance() {
19170            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("nothing") => {
19171                crate::ast::OnConflictAction::Nothing
19172            }
19173            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update") => {
19174                self.parse_on_conflict_update_action()?
19175            }
19176            other => {
19177                return Err(self.err(alloc::format!(
19178                    "expected NOTHING or UPDATE after ON CONFLICT DO, got {other:?}"
19179                )));
19180            }
19181        };
19182        Ok(Some(crate::ast::OnConflictClause {
19183            target_columns,
19184            index_where,
19185            constraint_name,
19186            mysql_lowered: false,
19187            action,
19188        }))
19189    }
19190
19191    /// v7.9.7 — tail of `ON CONFLICT … DO UPDATE`: parse
19192    /// `SET col = expr [, …] [WHERE cond]`. Caller already
19193    /// consumed `UPDATE`.
19194    fn parse_on_conflict_update_action(
19195        &mut self,
19196    ) -> Result<crate::ast::OnConflictAction, ParseError> {
19197        // `SET`
19198        match self.advance() {
19199            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("set") => {}
19200            other => {
19201                return Err(self.err(alloc::format!(
19202                    "expected SET after ON CONFLICT DO UPDATE, got {other:?}"
19203                )));
19204            }
19205        }
19206        let mut assignments: Vec<(String, Expr)> = Vec::new();
19207        loop {
19208            let col = self.expect_ident_like()?;
19209            if !matches!(self.peek(), Token::Eq) {
19210                return Err(self.err(alloc::format!(
19211                    "expected `=` after column in ON CONFLICT DO UPDATE SET, got {:?}",
19212                    self.peek()
19213                )));
19214            }
19215            self.advance();
19216            let value = self.parse_expr(0)?;
19217            assignments.push((col, value));
19218            if matches!(self.peek(), Token::Comma) {
19219                self.advance();
19220                continue;
19221            }
19222            break;
19223        }
19224        let where_ = if matches!(self.peek(), Token::Where) {
19225            self.advance();
19226            Some(self.parse_expr(0)?)
19227        } else {
19228            None
19229        };
19230        Ok(crate::ast::OnConflictAction::Update {
19231            assignments,
19232            where_,
19233        })
19234    }
19235
19236    fn parse_select_list(&mut self) -> Result<Vec<SelectItem>, ParseError> {
19237        let mut items = Vec::new();
19238        // v7.39 (round 341, V66) — PG's target list may be EMPTY
19239        // (`opt_target_list: target_list | /*EMPTY*/`): `SELECT FROM t`
19240        // answers one zero-column row per row of t, and a bare `SELECT`
19241        // answers a single zero-column row. SPG required at least one
19242        // item, so both were syntax errors. Recognised by the token that
19243        // follows — nothing that can start an expression appears here.
19244        if self.select_list_is_empty_here() {
19245            return Ok(items);
19246        }
19247        loop {
19248            items.push(self.parse_select_item()?);
19249            if matches!(self.peek(), Token::Comma) {
19250                self.advance();
19251            } else {
19252                break;
19253            }
19254        }
19255        Ok(items)
19256    }
19257
19258    /// Is the target list empty at this point — i.e. does the next token
19259    /// end the SELECT's item list rather than start an item?
19260    fn select_list_is_empty_here(&self) -> bool {
19261        match self.peek() {
19262            Token::From
19263            | Token::Where
19264            | Token::Group
19265            | Token::Having
19266            | Token::Order
19267            | Token::Limit
19268            | Token::Offset
19269            | Token::Semicolon
19270            | Token::RParen
19271            | Token::Union
19272            | Token::Except
19273            | Token::Eof => true,
19274            // `FETCH FIRST … ROWS ONLY` and `WINDOW w AS …` are spelled
19275            // with unreserved keywords, so they arrive as plain idents.
19276            Token::Ident(s) => {
19277                s.eq_ignore_ascii_case("fetch")
19278                    || s.eq_ignore_ascii_case("window")
19279                    || s.eq_ignore_ascii_case("intersect")
19280            }
19281            _ => false,
19282        }
19283    }
19284
19285    fn parse_select_item(&mut self) -> Result<SelectItem, ParseError> {
19286        if matches!(self.peek(), Token::Star) {
19287            self.advance();
19288            return Ok(SelectItem::Wildcard);
19289        }
19290        // v7.39 (read01 round 128) — qualified wildcard `qualifier.*`. Intercept
19291        // BEFORE `parse_expr`, which would treat `q.` as a qualified column and
19292        // choke on the `*` ("expected identifier, got Star"). The lookahead is
19293        // `<ident> . *` with nothing binding tighter.
19294        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone() {
19295            if matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
19296                && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
19297            {
19298                self.advance(); // qualifier
19299                self.advance(); // .
19300                self.advance(); // *
19301                return Ok(SelectItem::QualifiedWildcard(q));
19302            }
19303        }
19304        let start_tok = self.pos;
19305        let expr = self.parse_expr(0)?;
19306        let end_tok = self.consumed_pos();
19307        // v7.39 (read01 round 69) — `(f(args)).*`: expand the RECORD a
19308        // multi-column function returns into columns. Marked here and lowered in
19309        // `parse_bare_select`, where the FROM clause is in hand.
19310        if matches!(self.peek(), Token::Dot)
19311            && matches!(self.tokens.get(self.pos + 1), Some(Token::Star))
19312        {
19313            self.advance(); // .
19314            self.advance(); // *
19315            return Ok(SelectItem::Expr {
19316                expr: Expr::FunctionCall {
19317                    name: "__record_expand".to_string(),
19318                    args: alloc::vec![expr],
19319                },
19320                alias: None,
19321            });
19322        }
19323        // v7.39.2 — MySQL lets a STRING name a projection item, with or
19324        // without `AS`: `SELECT 1 'x'`, `SELECT COUNT(*) 'total'`,
19325        // `SELECT 1 'a b'` (which is why one quotes it). SPG answered
19326        // `syntax error at or near "'x'"` to all of them.
19327        //
19328        // Only here, not in `parse_optional_alias`: that one also names
19329        // TABLES, and MySQL 9.7.2 refuses a string there — `FROM t 'ta'`
19330        // and `FROM t AS 'ta'` are both syntax errors, measured. And only
19331        // after the lexer's own rule has joined adjacent literals, or
19332        // `SELECT 'a' 'b'` would read as a literal aliased `b` where
19333        // MySQL answers the concatenation `ab`.
19334        if self.mysql_dialect {
19335            let at_as = matches!(self.peek(), Token::As)
19336                && matches!(self.tokens.get(self.pos + 1), Some(Token::String(_)));
19337            if at_as {
19338                self.advance();
19339            }
19340            if let Token::String(name) = self.peek().clone() {
19341                self.advance();
19342                return Ok(SelectItem::Expr {
19343                    expr,
19344                    alias: Some(name),
19345                });
19346            }
19347        }
19348        let alias = match self.parse_optional_alias()? {
19349            Some(a) => Some(a),
19350            None => self.mysql_item_label(&expr, start_tok, end_tok),
19351        };
19352        Ok(SelectItem::Expr { expr, alias })
19353    }
19354
19355    /// v7.39 (round 506) — the name MariaDB 11 gives a projection item that
19356    /// carries no `AS`, filled in here so every downstream path reports it
19357    /// without knowing the rule. `None` leaves the item un-aliased, which is
19358    /// what a PG session always gets.
19359    ///
19360    /// Measured against MariaDB 11, three rules and no more:
19361    ///
19362    /// | item             | label      | why                          |
19363    /// |------------------|------------|------------------------------|
19364    /// | `lbl.a`          | `a`        | a column reports its name    |
19365    /// | `'it''s'`        | `it's`     | a string reports its VALUE   |
19366    /// | `a  +  b`        | `a  +  b`  | anything else, source text   |
19367    ///
19368    /// The third is why this lives in the parser at all: the label is the
19369    /// text the client WROTE, down to the spacing, so it cannot be printed
19370    /// back out of the parsed shape. `COUNT( * )` names itself `COUNT( * )`.
19371    ///
19372    /// Comments survive, and that is right: through a `mariadb` CLI both
19373    /// servers answer `a  + b` for `SELECT a /* c */ + b`, but that is the
19374    /// CLIENT stripping the comment before it sends. Asked over the raw
19375    /// protocol, MariaDB answers `a /* c */ + b` — byte for byte what this
19376    /// produces.
19377    fn mysql_item_label(&self, expr: &Expr, start_tok: usize, end_tok: usize) -> Option<String> {
19378        if !self.mysql_dialect {
19379            return None;
19380        }
19381        match expr {
19382            // A column already reports its own name downstream; naming it
19383            // again here would only re-state the qualifier the label drops.
19384            Expr::Column(_) => None,
19385            // v7.39.3 — `SELECT 'a' 'b'` is ONE literal whose value is
19386            // `ab`, and MySQL 9.7.2 names the column `a`: the label is
19387            // the first segment as written, not the joined value
19388            // (measured). The lexer logs where it joined them.
19389            Expr::Literal(Literal::String(v)) => Some(
19390                self.merged_first_len(start_tok)
19391                    .and_then(|n| v.get(..n))
19392                    .map_or_else(|| v.clone(), String::from),
19393            ),
19394            _ => self.source_span(start_tok, end_tok).map(str::to_string),
19395        }
19396    }
19397
19398    /// v7.37.17 (17.6 siblings) — parse `(row), (row), …` after a
19399    /// consumed VALUES keyword. Each row lowers to a constant SELECT
19400    /// with PG's default column1..columnN names; subsequent rows
19401    /// chain as UNION ALL peers. Shared by the FROM-position
19402    /// `( VALUES … )` arm and the top-level bare VALUES statement.
19403    fn parse_values_rows_body(&mut self) -> Result<SelectStatement, ParseError> {
19404        let mut row_selects: Vec<SelectStatement> = Vec::new();
19405        loop {
19406            if !matches!(self.peek(), Token::LParen) {
19407                return Err(self.err(alloc::format!(
19408                    "expected '(' to start a VALUES row, got {:?}",
19409                    self.peek()
19410                )));
19411            }
19412            self.advance(); // (
19413            let mut items: Vec<SelectItem> = Vec::new();
19414            loop {
19415                let expr = self.parse_expr(0)?;
19416                items.push(SelectItem::Expr {
19417                    expr,
19418                    alias: Some(alloc::format!("column{}", items.len() + 1)),
19419                });
19420                match self.peek() {
19421                    Token::Comma => {
19422                        self.advance();
19423                    }
19424                    Token::RParen => break,
19425                    other => {
19426                        return Err(self.err(alloc::format!(
19427                            "expected ',' or ')' in VALUES row, got {other:?}"
19428                        )));
19429                    }
19430                }
19431            }
19432            self.advance(); // )
19433            row_selects.push(SelectStatement {
19434                locking: None,
19435                ctes: Vec::new(),
19436                distinct: false,
19437                distinct_on: Vec::new(),
19438                items,
19439                from: None,
19440                where_: None,
19441                group_by: None,
19442                group_by_all: false,
19443                having: None,
19444                unions: Vec::new(),
19445                order_by: Vec::new(),
19446                limit: None,
19447                offset: None,
19448                limit_with_ties: false,
19449                window_check_exprs: Vec::new(),
19450            });
19451            if matches!(self.peek(), Token::Comma) {
19452                self.advance();
19453                continue;
19454            }
19455            break;
19456        }
19457        let mut head = row_selects.remove(0);
19458        head.unions = row_selects
19459            .into_iter()
19460            .map(|s| (UnionKind::All, s))
19461            .collect();
19462        Ok(head)
19463    }
19464
19465    fn parse_table_ref(&mut self) -> Result<TableRef, ParseError> {
19466        // v7.39 (round 621) — `FROM ONLY <table>` excludes a table's
19467        // children. It was read as a table NAMED `only`, so the query
19468        // failed on `relation "only" does not exist`.
19469        //
19470        // v7.39 (round 644) — and it is no longer a no-op. Round 621
19471        // absorbed the keyword, reasoning that SPG's children are
19472        // separate relations a plain scan does not descend into, so ONLY
19473        // already described the scan. That stopped being true when a
19474        // partition parent started unioning its children: measured,
19475        // `SELECT count(*) FROM ONLY <partitioned parent>` answered 2
19476        // where PG answers 0. The flag is carried now.
19477        let mut only = false;
19478        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("only"))
19479            && matches!(
19480                self.tokens.get(self.pos + 1),
19481                Some(Token::Ident(_) | Token::QuotedIdent(_))
19482            )
19483        {
19484            only = true;
19485            self.advance();
19486        }
19487        // `LATERAL generate_series(...)` / `LATERAL unnest(...)` —
19488        // for these SRFs the keyword is noise at parse time: the
19489        // join executor already substitutes outer-column references
19490        // into unnest_expr / generate_series_args per outer row
19491        // (v7.37.43-T4.5 substitute_outer_in_table_ref), and PG
19492        // licences the correlation even without the keyword. Absorb
19493        // it and fall through to the SRF arms below.
19494        // v7.39 (read01 round 69) — `LATERAL <fn>(args)` for ANY function, not
19495        // just the four builtin SRFs: a user set-returning function on a JOIN's
19496        // right side is the whole point of LATERAL. The keyword stays noise at
19497        // parse time — the join executor substitutes the outer row into the
19498        // call's arguments per outer row.
19499        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19500            && matches!(
19501                self.tokens.get(self.pos + 1),
19502                // The json_each family has its OWN `LATERAL …` arm below, which
19503                // needs to see the keyword — absorbing it here would send those
19504                // calls down the generic table-function channel instead.
19505                Some(Token::Ident(s) | Token::QuotedIdent(s)) if !is_json_each_name(s)
19506            )
19507            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19508        {
19509            self.advance(); // LATERAL
19510        }
19511        // v7.37.43-T4.5 — `LATERAL jsonb_each_text(<expr>)` —
19512        // set-returning function whose argument may reference a
19513        // preceding FROM item. We rewrite this to
19514        // `LATERAL (SELECT key, value FROM jsonb_each_text(<expr>)
19515        // AS __srf__) AS <alias>` so the existing LATERAL subquery
19516        // executor handles per-outer-row evaluation and the
19517        // SRF-primary jsonb_each_text path handles the inner
19518        // materialisation. Sentori 0067 backfill is the dogfood
19519        // shape.
19520        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19521            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if is_json_each_name(s))
19522            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
19523        {
19524            self.advance(); // LATERAL
19525            let each_fn = match self.peek() {
19526                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19527                _ => unreachable!(),
19528            };
19529            self.advance(); // jsonb_each[_text] / json_each[_text]
19530            self.advance(); // (
19531            let arg = self.parse_expr(0)?;
19532            if !matches!(self.peek(), Token::RParen) {
19533                return Err(self.err(alloc::format!(
19534                    "expected ')' after LATERAL {each_fn}() argument, got {:?}",
19535                    self.peek()
19536                )));
19537            }
19538            self.advance();
19539            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19540            let alias = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19541            // Synthesise: SELECT __srf__.key AS <key_alias>, __srf__.value AS <value_alias>
19542            //               FROM jsonb_each_text(<arg>) AS __srf__
19543            // PG's `AS kv(key, value)` column-alias list maps
19544            // positions to names; default to (key, value) when
19545            // omitted (matching the SRF's natural column names).
19546            let srf_alias = "__srf__".to_string();
19547            let key_alias = column_aliases
19548                .first()
19549                .cloned()
19550                .unwrap_or_else(|| "key".to_string());
19551            let value_alias = column_aliases
19552                .get(1)
19553                .cloned()
19554                .unwrap_or_else(|| "value".to_string());
19555            let inner_select = crate::ast::SelectStatement {
19556                locking: None,
19557                ctes: Vec::new(),
19558                distinct: false,
19559                distinct_on: Vec::new(),
19560                items: alloc::vec![
19561                    crate::ast::SelectItem::Expr {
19562                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19563                            qualifier: Some(srf_alias.clone()),
19564                            name: "key".to_string(),
19565                        }),
19566                        alias: Some(key_alias),
19567                    },
19568                    crate::ast::SelectItem::Expr {
19569                        expr: crate::ast::Expr::Column(crate::ast::ColumnName {
19570                            qualifier: Some(srf_alias.clone()),
19571                            name: "value".to_string(),
19572                        }),
19573                        alias: Some(value_alias),
19574                    },
19575                ],
19576                from: Some(crate::ast::FromClause {
19577                    primary: TableRef {
19578                        name: srf_alias.clone(),
19579                        alias: Some(srf_alias.clone()),
19580                        only: false,
19581                        as_of_segment: None,
19582                        unnest_expr: None,
19583                        unnest_column_aliases: Vec::new(),
19584                        with_ordinality: false,
19585                        generate_series_args: None,
19586                        lateral_subquery: None,
19587                        jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19588                        table_fn_call: None,
19589                        rows_from: None,
19590                        json_table: None,
19591                        scalar_fn_item: false,
19592                    },
19593                    joins: Vec::new(),
19594                }),
19595                where_: None,
19596                group_by: None,
19597                group_by_all: false,
19598                having: None,
19599                unions: Vec::new(),
19600                order_by: Vec::new(),
19601                limit: None,
19602                offset: None,
19603                limit_with_ties: false,
19604                window_check_exprs: Vec::new(),
19605            };
19606            return Ok(TableRef {
19607                name: alias.clone(),
19608                alias: Some(alias),
19609                only: false,
19610                as_of_segment: None,
19611                unnest_expr: None,
19612                unnest_column_aliases: Vec::new(),
19613                with_ordinality: false,
19614                generate_series_args: None,
19615                lateral_subquery: Some(Box::new(inner_select)),
19616                jsonb_each_text_arg: None,
19617                table_fn_call: None,
19618                rows_from: None,
19619                json_table: None,
19620                scalar_fn_item: false,
19621            });
19622        }
19623        // v7.37.43-T4.5 — bare `CROSS JOIN jsonb_each_text(t.col)`
19624        // without an explicit `LATERAL` keyword is the same shape
19625        // PG accepts (SRF naturally licences lateral correlation).
19626        // We mirror the LATERAL rewrite when the argument syntactic-
19627        // ally references an outer column (Column { qualifier:
19628        // Some(_), … }). For simplicity we apply the rewrite
19629        // whenever the SRF directly follows JOIN/CROSS JOIN/comma
19630        // in the FROM-list — caller-side join parsing positions
19631        // this peek correctly.
19632        // (Implementation note: detection lives below; the LATERAL
19633        // branch above already covers the explicit form; the bare
19634        // form falls through to the plain SRF arm and the engine
19635        // treats it as a constant-arg SRF if no outer reference is
19636        // present.)
19637        // v7.17.0 Phase 3.P0-41 — `LATERAL ( SELECT … )` derived
19638        // table. Detect at the head so it claims precedence over
19639        // every other table-ref shape (unnest / generate_series /
19640        // bare ident); the lateral subquery itself follows the
19641        // regular SELECT grammar.
19642        // v7.37.17 (17.6 siblings) — `FROM ( VALUES (…), (…) ) [AS]
19643        // t(cols)`. Each row lowers to a constant SELECT with PG's
19644        // default column1..columnN names; subsequent rows chain as
19645        // UNION ALL peers. The result rides the derived-table
19646        // lateral_subquery channel — zero executor work.
19647        if matches!(self.peek(), Token::LParen)
19648            && matches!(self.tokens.get(self.pos + 1), Some(Token::Values))
19649        {
19650            self.advance(); // (
19651            self.advance(); // VALUES
19652            let head = self.parse_values_rows_body()?;
19653            if !matches!(self.peek(), Token::RParen) {
19654                return Err(self.err(alloc::format!(
19655                    "expected ')' after VALUES list, got {:?}",
19656                    self.peek()
19657                )));
19658            }
19659            self.advance();
19660            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19661            let name = alias_ident.clone().unwrap_or_else(|| "values".to_string());
19662            return Ok(TableRef {
19663                name,
19664                alias: alias_ident,
19665                only: false,
19666                as_of_segment: None,
19667                unnest_expr: None,
19668                unnest_column_aliases: column_aliases,
19669                with_ordinality: false,
19670                generate_series_args: None,
19671                lateral_subquery: Some(Box::new(head)),
19672                jsonb_each_text_arg: None,
19673                table_fn_call: None,
19674                rows_from: None,
19675                json_table: None,
19676                scalar_fn_item: false,
19677            });
19678        }
19679        // v7.37.17 (17.6 siblings) — plain derived table:
19680        // `FROM ( SELECT … ) [AS] alias`. Rides the same
19681        // lateral_subquery channel the explicit LATERAL form uses —
19682        // an uncorrelated inner SELECT executes identically. The
19683        // inner parse carries UNION tails (they live on
19684        // SelectStatement.unions).
19685        // v7.37 D.20 — the derived-table inner may itself be a
19686        // parenthesized set-operation group (`FROM ((SELECT…) UNION
19687        // (SELECT…)) s`) or a CTE (`FROM (WITH … SELECT …) z`), not just a
19688        // bare `(SELECT …)`. parse_one_statement already routes a leading
19689        // `(` set-op group (its LParen arm) and a leading WITH
19690        // (parse_with_cte_then_select), so widen the second-token gate to
19691        // Select | LParen | WITH.
19692        // v7.39 (round 869) — `Table` joins that gate. `TABLE t` is
19693        // PG's spelling of `SELECT * FROM t` and is accepted wherever a
19694        // SELECT is, so `FROM (TABLE t) x` has to parse. The desugaring
19695        // has existed since the shorthand landed and `parse_bare_select`
19696        // already routes it ("valid anywhere a SELECT head is"); what was
19697        // missing is this second-token gate, and the CTE body's dispatch
19698        // below. Round 868 found both by putting the shorthand in a
19699        // subquery — the top-level forms had been the only ones tested.
19700        if matches!(self.peek(), Token::LParen)
19701            && (matches!(
19702                self.tokens.get(self.pos + 1),
19703                Some(Token::Select | Token::LParen | Token::Table)
19704            ) || matches!(self.tokens.get(self.pos + 1),
19705                    Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("with")))
19706        {
19707            self.advance(); // (
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 derived table ( … ), got {other:?}"
19713                    )));
19714                }
19715            };
19716            if !matches!(self.peek(), Token::RParen) {
19717                return Err(self.err(alloc::format!(
19718                    "expected ')' after derived-table subquery, got {:?}",
19719                    self.peek()
19720                )));
19721            }
19722            self.advance();
19723            // `AS t(a, b)` column-alias list rides the
19724            // unnest_column_aliases field (same positional-rename
19725            // contract the unnest SRFs use).
19726            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19727            let name = alias_ident
19728                .clone()
19729                .unwrap_or_else(|| "subquery".to_string());
19730            return Ok(TableRef {
19731                name,
19732                alias: alias_ident,
19733                only: false,
19734                as_of_segment: None,
19735                unnest_expr: None,
19736                unnest_column_aliases: column_aliases,
19737                with_ordinality: false,
19738                generate_series_args: None,
19739                lateral_subquery: Some(Box::new(inner)),
19740                jsonb_each_text_arg: None,
19741                table_fn_call: None,
19742                rows_from: None,
19743                json_table: None,
19744                scalar_fn_item: false,
19745            });
19746        }
19747        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("lateral"))
19748            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19749        {
19750            self.advance(); // LATERAL
19751            self.advance(); // (
19752            // Parse the inner SELECT.
19753            let inner = match self.parse_one_statement()? {
19754                Statement::Select(s) => s,
19755                other => {
19756                    return Err(self.err(alloc::format!(
19757                        "expected SELECT inside LATERAL ( … ), got {other:?}"
19758                    )));
19759                }
19760            };
19761            if !matches!(self.peek(), Token::RParen) {
19762                return Err(self.err(alloc::format!(
19763                    "expected ')' after LATERAL subquery, got {:?}",
19764                    self.peek()
19765                )));
19766            }
19767            self.advance();
19768            // v7.37 D.28 — `LATERAL (…) AS t(cols)` column-alias list (also how a
19769            // `(VALUES …) t(g)` derived table round-trips through view-body
19770            // Display, which renders on the lateral_subquery channel).
19771            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19772            let name = alias_ident.clone().unwrap_or_else(|| "lateral".to_string());
19773            return Ok(TableRef {
19774                name,
19775                alias: alias_ident,
19776                only: false,
19777                as_of_segment: None,
19778                unnest_expr: None,
19779                unnest_column_aliases: column_aliases,
19780                with_ordinality: false,
19781                generate_series_args: None,
19782                lateral_subquery: Some(Box::new(inner)),
19783                jsonb_each_text_arg: None,
19784                table_fn_call: None,
19785                rows_from: None,
19786                json_table: None,
19787                scalar_fn_item: false,
19788            });
19789        }
19790        // v7.37.43-T4.5 — `jsonb_each_text(<expr>)` set-returning
19791        // function as a FROM item. Emits one row per (key, value)
19792        // pair in the JSONB object argument as TEXT columns. May
19793        // be wrapped in CROSS JOIN LATERAL when the argument
19794        // references a preceding FROM item (sentori migration
19795        // 0067 backfill shape: `CROSS JOIN LATERAL
19796        // jsonb_each_text(t.json_col) AS kv(key, value)`).
19797        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_each_name(s))
19798            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19799        {
19800            let each_fn = match self.peek() {
19801                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19802                _ => unreachable!(),
19803            };
19804            self.advance(); // jsonb_each[_text] / json_each[_text]
19805            self.advance(); // (
19806            let arg = self.parse_expr(0)?;
19807            if !matches!(self.peek(), Token::RParen) {
19808                return Err(self.err(alloc::format!(
19809                    "expected ')' after {each_fn}() argument, got {:?}",
19810                    self.peek()
19811                )));
19812            }
19813            self.advance();
19814            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19815            let name = alias_ident.clone().unwrap_or_else(|| each_fn.clone());
19816            return Ok(TableRef {
19817                name,
19818                alias: alias_ident,
19819                only: false,
19820                as_of_segment: None,
19821                unnest_expr: None,
19822                // `AS t(k, v)` renames key/value positionally, same as the
19823                // LATERAL-position form already does.
19824                unnest_column_aliases: column_aliases,
19825                with_ordinality: false,
19826                generate_series_args: None,
19827                lateral_subquery: None,
19828                jsonb_each_text_arg: Some((each_fn, Box::new(arg))),
19829                table_fn_call: None,
19830                rows_from: None,
19831                json_table: None,
19832                scalar_fn_item: false,
19833            });
19834        }
19835        // `jsonb_to_recordset(J) AS t(a int, b text)` / `jsonb_to_record`
19836        // (+ json_ variants) — record-returning JSON functions with a
19837        // column-definition list. Desugar to a derived table that
19838        // projects each declared column from the JSON via `->>` + a cast,
19839        // over `jsonb_array_elements(J)` for the *set (per-element) form.
19840        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if is_json_to_record_name(s))
19841            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19842        {
19843            return self.parse_json_to_record_from();
19844        }
19845        // v7.38 (T15) — `regexp_matches(s, pat[, flags])` as a FROM item. Each
19846        // row is a text[] of capture groups, so it cannot desugar to unnest
19847        // (that would flatten the array). Wrap it as a derived table
19848        // `(SELECT regexp_matches(args)) AS <alias>(<col>)` — the SELECT-list
19849        // SRF path already emits one text[] row per match. PG names the column
19850        // `regexp_matches`; an `AS a(col)` alias overrides it.
19851        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19852                if s.eq_ignore_ascii_case("regexp_matches"))
19853            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19854        {
19855            self.advance(); // fn name
19856            self.advance(); // (
19857            let mut fn_args: Vec<Expr> = Vec::new();
19858            loop {
19859                fn_args.push(self.parse_expr(0)?);
19860                if matches!(self.peek(), Token::Comma) {
19861                    self.advance();
19862                    continue;
19863                }
19864                break;
19865            }
19866            if !matches!(self.peek(), Token::RParen) {
19867                return Err(self.err(alloc::format!(
19868                    "expected ')' after regexp_matches() arguments, got {:?}",
19869                    self.peek()
19870                )));
19871            }
19872            self.advance();
19873            // v7.39 (read01 round 78) — WITH ORDINALITY sits BEFORE the alias
19874            // (`f(x) WITH ORDINALITY AS t(v, o)`), and this arm never looked for
19875            // it, so it died on the `with` token while every other table function
19876            // accepted it.
19877            let with_ordinality = self.absorb_with_ordinality();
19878            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19879            let table_alias = alias_ident
19880                .clone()
19881                .unwrap_or_else(|| "regexp_matches".to_string());
19882            // PG names a single-column function's output column after the ALIAS
19883            // when one is given (`FROM regexp_matches(…) AS m` → column `m`), so
19884            // `m` reads as that column and not as a whole-row composite. Naming
19885            // it after the function regardless made `SELECT m[1] FROM … AS m`
19886            // subscript a record.
19887            let col_name = column_aliases
19888                .first()
19889                .cloned()
19890                .or_else(|| alias_ident.clone())
19891                .unwrap_or_else(|| "regexp_matches".to_string());
19892            let inner = crate::ast::SelectStatement {
19893                locking: None,
19894                ctes: Vec::new(),
19895                distinct: false,
19896                distinct_on: Vec::new(),
19897                items: alloc::vec![SelectItem::Expr {
19898                    expr: Expr::FunctionCall {
19899                        name: "regexp_matches".to_string(),
19900                        args: fn_args,
19901                    },
19902                    alias: Some(col_name),
19903                }],
19904                from: None,
19905                where_: None,
19906                group_by: None,
19907                group_by_all: false,
19908                having: None,
19909                unions: Vec::new(),
19910                order_by: Vec::new(),
19911                limit: None,
19912                offset: None,
19913                limit_with_ties: false,
19914                window_check_exprs: Vec::new(),
19915            };
19916            return Ok(TableRef {
19917                name: table_alias.clone(),
19918                alias: Some(table_alias),
19919                only: false,
19920                as_of_segment: None,
19921                unnest_expr: None,
19922                unnest_column_aliases: column_aliases,
19923                with_ordinality,
19924                generate_series_args: None,
19925                lateral_subquery: Some(Box::new(inner)),
19926                jsonb_each_text_arg: None,
19927                table_fn_call: None,
19928                rows_from: None,
19929                json_table: None,
19930                // regexp_matches returns text[], a base type: `SELECT m FROM
19931                // regexp_matches(…) AS m` is the array, not a composite wrapping it.
19932                scalar_fn_item: true,
19933            });
19934        }
19935        // v7.37.17 (17.6 siblings) — `jsonb_array_elements[_text](<expr>)`
19936        // / json_ variants as a FROM item. Rewritten into
19937        // `unnest(<same fn>(<expr>))`: the scalar form returns the
19938        // elements as a TEXT array, and the existing unnest SRF path
19939        // materialises one row per element. PG's natural column name
19940        // is `value`; an `AS a(col)` column-alias list overrides it.
19941        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
19942                if s.eq_ignore_ascii_case("jsonb_array_elements")
19943                    || s.eq_ignore_ascii_case("json_array_elements")
19944                    || s.eq_ignore_ascii_case("jsonb_array_elements_text")
19945                    || s.eq_ignore_ascii_case("json_array_elements_text")
19946                    || s.eq_ignore_ascii_case("jsonb_object_keys")
19947                    || s.eq_ignore_ascii_case("json_object_keys")
19948                    || s.eq_ignore_ascii_case("jsonb_path_query")
19949                    || s.eq_ignore_ascii_case("json_path_query")
19950                    || s.eq_ignore_ascii_case("generate_subscripts")
19951                    || s.eq_ignore_ascii_case("string_to_table")
19952                    || s.eq_ignore_ascii_case("regexp_split_to_table"))
19953            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
19954        {
19955            let fn_name = match self.peek() {
19956                Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
19957                _ => unreachable!(),
19958            };
19959            self.advance(); // fn name
19960            self.advance(); // (
19961            let mut fn_args: Vec<Expr> = Vec::new();
19962            loop {
19963                fn_args.push(self.parse_expr(0)?);
19964                if matches!(self.peek(), Token::Comma) {
19965                    self.advance();
19966                    continue;
19967                }
19968                break;
19969            }
19970            if !matches!(self.peek(), Token::RParen) {
19971                return Err(self.err(alloc::format!(
19972                    "expected ')' after {fn_name}() arguments, got {:?}",
19973                    self.peek()
19974                )));
19975            }
19976            self.advance();
19977            let with_ordinality = self.absorb_with_ordinality();
19978            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
19979            let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
19980            // PG's natural column name: the array-elements SRFs
19981            // declare an OUT parameter `value`; jsonb_object_keys
19982            // and generate_subscripts have none, so the column is
19983            // named after the function. A bare table alias on a
19984            // single-column SRF renames the column too (PG: `FROM
19985            // generate_subscripts(a, 1) AS s` projects column s) —
19986            // except for the OUT-parameter SRFs, whose column stays
19987            // `value` under a bare alias.
19988            let natural_col = if fn_name.ends_with("_array_elements")
19989                || fn_name.ends_with("_array_elements_text")
19990            {
19991                "value".to_string()
19992            } else {
19993                alias_ident.clone().unwrap_or_else(|| fn_name.clone())
19994            };
19995            let mut srf_cols = alloc::vec![column_aliases.first().cloned().unwrap_or(natural_col)];
19996            // Keep any further entries — the second names the
19997            // ordinality column under WITH ORDINALITY.
19998            srf_cols.extend(column_aliases.into_iter().skip(1));
19999            // The *_to_table SRFs are row-streams over the existing
20000            // *_to_array scalars — map the call target; the display
20001            // name (alias / column defaults) keeps the SRF spelling.
20002            let call_name = match fn_name.as_str() {
20003                "string_to_table" => "string_to_array".to_string(),
20004                "regexp_split_to_table" => "regexp_split_to_array".to_string(),
20005                _ => fn_name,
20006            };
20007            // v7.38 (read01, T-srf/T-lateral) — an SRF argument that references a
20008            // preceding FROM item (bare or qualified column) is correlated;
20009            // route it through the per-outer-row lateral channel.
20010            let expr = crate::ast::Expr::FunctionCall {
20011                name: call_name,
20012                args: fn_args,
20013            };
20014            let correlated = Self::expr_has_any_column(&expr);
20015            let tref = TableRef {
20016                name,
20017                alias: alias_ident,
20018                only: false,
20019                as_of_segment: None,
20020                unnest_expr: Some(Box::new(expr)),
20021                unnest_column_aliases: srf_cols,
20022                with_ordinality,
20023                generate_series_args: None,
20024                lateral_subquery: None,
20025                jsonb_each_text_arg: None,
20026                table_fn_call: None,
20027                rows_from: None,
20028                json_table: None,
20029                // Each of these returns a BASE type (jsonb / text / int), so the item's
20030                // row type is that scalar: `SELECT j FROM jsonb_array_elements('[1]') j`
20031                // is `1`, not `(1)`. WITH ORDINALITY makes it a real two-column item.
20032                scalar_fn_item: !with_ordinality,
20033            };
20034            return Ok(if correlated {
20035                Self::wrap_correlated_srf(tref)
20036            } else {
20037                tref
20038            });
20039        }
20040        // `ROWS FROM ( srf(args) [, srf(args)]* )` — SQL-standard
20041        // explicit parallel-zip syntax. Each entry lowers to its
20042        // array-returning scalar form (unnest(x) → x itself; the
20043        // FROM-SRF rewrite family → their scalar array calls) and
20044        // the list rides the multi-arg unnest zip channel:
20045        // NULL-padded to the longest, WITH ORDINALITY appends the
20046        // counter. generate_series has no scalar array form and
20047        // errors honestly.
20048        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("rows"))
20049            && matches!(self.tokens.get(self.pos + 1), Some(Token::From))
20050            && matches!(self.tokens.get(self.pos + 2), Some(Token::LParen))
20051        {
20052            self.advance(); // ROWS
20053            self.advance(); // FROM
20054            self.advance(); // (
20055            let mut entries: Vec<Expr> = Vec::new();
20056            // v7.39 (read01 round 74) — the generic channel, filled in parallel.
20057            // Used only when some entry has no array form.
20058            let mut generic: Vec<(String, Vec<Expr>)> = Vec::new();
20059            loop {
20060                let fn_name = self.expect_ident_like()?.to_ascii_lowercase();
20061                if !matches!(self.peek(), Token::LParen) {
20062                    return Err(self.err(alloc::format!(
20063                        "expected '(' after {fn_name} in ROWS FROM, got {:?}",
20064                        self.peek()
20065                    )));
20066                }
20067                self.advance();
20068                let mut fn_args: Vec<Expr> = Vec::new();
20069                if !matches!(self.peek(), Token::RParen) {
20070                    loop {
20071                        fn_args.push(self.parse_expr(0)?);
20072                        if matches!(self.peek(), Token::Comma) {
20073                            self.advance();
20074                            continue;
20075                        }
20076                        break;
20077                    }
20078                }
20079                if !matches!(self.peek(), Token::RParen) {
20080                    return Err(self.err(alloc::format!(
20081                        "expected ')' after {fn_name}() arguments in ROWS FROM, got {:?}",
20082                        self.peek()
20083                    )));
20084                }
20085                self.advance();
20086                let entry = match fn_name.as_str() {
20087                    "unnest" => {
20088                        if fn_args.len() != 1 {
20089                            return Err(
20090                                self.err("unnest inside ROWS FROM takes exactly one array".into())
20091                            );
20092                        }
20093                        fn_args.pop().expect("len checked")
20094                    }
20095                    "jsonb_array_elements"
20096                    | "json_array_elements"
20097                    | "jsonb_array_elements_text"
20098                    | "json_array_elements_text"
20099                    | "jsonb_object_keys"
20100                    | "json_object_keys"
20101                    | "generate_subscripts" => crate::ast::Expr::FunctionCall {
20102                        name: fn_name,
20103                        args: fn_args,
20104                    },
20105                    "string_to_table" => crate::ast::Expr::FunctionCall {
20106                        name: "string_to_array".to_string(),
20107                        args: fn_args,
20108                    },
20109                    "regexp_split_to_table" => crate::ast::Expr::FunctionCall {
20110                        name: "regexp_split_to_array".to_string(),
20111                        args: fn_args,
20112                    },
20113                    // v7.39 (read01 round 74) — an SRF with no array form
20114                    // (`generate_series`, a user `RETURNS SETOF` function) has no
20115                    // scalar expression to zip, so the WHOLE list switches to the
20116                    // rows_from channel, which runs each function and zips the
20117                    // rows themselves. The all-array case keeps the old lowering:
20118                    // it is well-trodden and this must not disturb it.
20119                    _ => {
20120                        generic.push((fn_name, fn_args));
20121                        if matches!(self.peek(), Token::Comma) {
20122                            self.advance();
20123                            continue;
20124                        }
20125                        break;
20126                    }
20127                };
20128                generic.push((
20129                    // The array-able entries carry their lowered expr along, so a
20130                    // MIXED list still works: the engine sees the scalar array
20131                    // form and unnests it.
20132                    "__array".to_string(),
20133                    alloc::vec![entry.clone()],
20134                ));
20135                entries.push(entry);
20136                if matches!(self.peek(), Token::Comma) {
20137                    self.advance();
20138                    continue;
20139                }
20140                break;
20141            }
20142            if !matches!(self.peek(), Token::RParen) {
20143                return Err(self.err(alloc::format!(
20144                    "expected ')' to close ROWS FROM, got {:?}",
20145                    self.peek()
20146                )));
20147            }
20148            self.advance();
20149            let with_ordinality = self.absorb_with_ordinality();
20150            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20151            let name = alias_ident.clone().unwrap_or_else(|| "rows".to_string());
20152            // v7.39 (read01 round 74) — some entry had no array form, so the whole
20153            // list rides the generic channel.
20154            if generic.iter().any(|(n, _)| n != "__array") {
20155                let correlated = generic
20156                    .iter()
20157                    .any(|(_, a)| a.iter().any(Self::expr_has_any_column));
20158                let tref = TableRef {
20159                    name,
20160                    alias: alias_ident,
20161                    only: false,
20162                    as_of_segment: None,
20163                    unnest_expr: None,
20164                    unnest_column_aliases,
20165                    with_ordinality,
20166                    generate_series_args: None,
20167                    lateral_subquery: None,
20168                    jsonb_each_text_arg: None,
20169                    table_fn_call: None,
20170                    rows_from: Some(generic),
20171                    json_table: None,
20172                    scalar_fn_item: false,
20173                };
20174                return Ok(if correlated {
20175                    Self::wrap_correlated_srf(tref)
20176                } else {
20177                    tref
20178                });
20179            }
20180            let correlated = entries.iter().any(Self::expr_has_any_column);
20181            let expr = if entries.len() == 1 {
20182                entries.pop().expect("len checked")
20183            } else {
20184                crate::ast::Expr::FunctionCall {
20185                    name: "__unnest_zip".to_string(),
20186                    args: entries,
20187                }
20188            };
20189            let tref = TableRef {
20190                name,
20191                alias: alias_ident,
20192                only: false,
20193                as_of_segment: None,
20194                unnest_expr: Some(Box::new(expr)),
20195                unnest_column_aliases,
20196                with_ordinality,
20197                generate_series_args: None,
20198                lateral_subquery: None,
20199                jsonb_each_text_arg: None,
20200                table_fn_call: None,
20201                rows_from: None,
20202                json_table: None,
20203                scalar_fn_item: false,
20204            };
20205            return Ok(if correlated {
20206                Self::wrap_correlated_srf(tref)
20207            } else {
20208                tref
20209            });
20210        }
20211        // v7.11.7 — `FROM unnest(<expr>) [AS] <alias>` set-returning
20212        // source. Detect at the head before the bare-ident fallback;
20213        // unnest is not a reserved token.
20214        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("unnest"))
20215            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20216        {
20217            self.advance(); // unnest
20218            self.advance(); // (
20219            let mut srf_args = alloc::vec![self.parse_expr(0)?];
20220            while matches!(self.peek(), Token::Comma) {
20221                self.advance();
20222                srf_args.push(self.parse_expr(0)?);
20223            }
20224            if !matches!(self.peek(), Token::RParen) {
20225                return Err(self.err(alloc::format!(
20226                    "expected ')' after unnest() argument, got {:?}",
20227                    self.peek()
20228                )));
20229            }
20230            self.advance();
20231            // Multi-arg unnest(a, b, …) zips the arrays in
20232            // parallel, NULL-padding to the longest (PG's ROWS
20233            // FROM shorthand). Lower onto the unnest channel as an
20234            // internal marker call the executors unpack.
20235            let expr = if srf_args.len() == 1 {
20236                srf_args.pop().expect("len checked")
20237            } else {
20238                crate::ast::Expr::FunctionCall {
20239                    name: "__unnest_zip".to_string(),
20240                    args: srf_args,
20241                }
20242            };
20243            let with_ordinality = self.absorb_with_ordinality();
20244            let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
20245            let name = alias_ident.clone().unwrap_or_else(|| "unnest".to_string());
20246            let correlated = Self::expr_has_any_column(&expr);
20247            let tref = TableRef {
20248                name,
20249                alias: alias_ident,
20250                only: false,
20251                as_of_segment: None,
20252                unnest_expr: Some(Box::new(expr)),
20253                unnest_column_aliases,
20254                with_ordinality,
20255                generate_series_args: None,
20256                lateral_subquery: None,
20257                jsonb_each_text_arg: None,
20258                table_fn_call: None,
20259                rows_from: None,
20260                json_table: None,
20261                scalar_fn_item: false,
20262            };
20263            return Ok(if correlated {
20264                Self::wrap_correlated_srf(tref)
20265            } else {
20266                tref
20267            });
20268        }
20269        // v7.39 (round 205, JSON_TABLE epic) — `JSON_TABLE(doc, '$path'
20270        // COLUMNS (...))` has bespoke syntax (a COLUMNS clause the
20271        // generic table-fn arg parser can't read), so it is intercepted
20272        // here BEFORE the generic dispatch. The doc expr may reference
20273        // outer columns (implicit LATERAL) — same correlated-wrap rule.
20274        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20275                if s.eq_ignore_ascii_case("json_table"))
20276            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20277        {
20278            let tref = self.parse_json_table_ref()?;
20279            let correlated = tref
20280                .json_table
20281                .as_deref()
20282                .is_some_and(|jt| Self::expr_has_any_column(&jt.doc));
20283            return Ok(if correlated {
20284                Self::wrap_correlated_srf(tref)
20285            } else {
20286                tref
20287            });
20288        }
20289        // v7.39 (read01 partitionfuncs.c) — generic FROM-position table
20290        // functions dispatched by name (`pg_partition_tree('t')`,
20291        // `pg_partition_ancestors('t')`). Same head-detection shape as
20292        // unnest; the engine executor owns the row shape per function.
20293        // v7.39 (read01 round 65) — and a USER function in FROM position
20294        // (`FROM rows_of(2)`). The SRFs with their own FROM pipeline
20295        // (generate_series / unnest / the json_each family) keep it — their arms
20296        // sit further down, so they are excluded here by name rather than by
20297        // ordering. Anything else that is an ident followed by `(` is a table
20298        // function; the engine executor decides whether it is a builtin, a
20299        // set-returning user function, or an error.
20300        // 7.38.1 S5.1 — pg_dump spells its table functions
20301        // schema-qualified (`pg_catalog.pg_options_to_table(...)`);
20302        // strip the pg_catalog prefix here so the same head-detection
20303        // fires. Only pg_catalog: a user schema's `s.f(x)` keeps its
20304        // meaning.
20305        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
20306            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
20307            && matches!(
20308                self.tokens.get(self.pos + 2),
20309                Some(Token::Ident(_) | Token::QuotedIdent(_))
20310            )
20311            && matches!(self.tokens.get(self.pos + 3), Some(Token::LParen))
20312        {
20313            self.advance(); // pg_catalog
20314            self.advance(); // .
20315        }
20316        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
20317                if !s.eq_ignore_ascii_case("generate_series")
20318                    && !s.eq_ignore_ascii_case("unnest")
20319                    && !is_json_each_name(s))
20320            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20321        {
20322            // Body out-of-line — this parse sits on the FROM/subquery
20323            // recursion chain (debug frame-cliff discipline).
20324            // v7.39 (read01 round 69) — a call whose arguments reference an outer
20325            // column (`t JOIN LATERAL dbl(t.id)`) is CORRELATED: it runs once per
20326            // outer row, so it rides the lateral channel. Same rule the unnest
20327            // arm uses.
20328            let tref = self.parse_table_fn_ref()?;
20329            let correlated = tref
20330                .table_fn_call
20331                .as_deref()
20332                .is_some_and(|(_, args)| args.iter().any(Self::expr_has_any_column));
20333            return Ok(if correlated {
20334                Self::wrap_correlated_srf(tref)
20335            } else {
20336                tref
20337            });
20338        }
20339        // v7.17.0 Phase 3.10 — `FROM generate_series(start, stop
20340        // [, step])` set-returning source. Same shape as unnest:
20341        // detect at the head, parse the comma-separated arg list,
20342        // dispatch downstream through the engine's set-returning
20343        // path. Supports integer triplets (mailrs's `WITH row_no AS
20344        // (SELECT * FROM generate_series(1, N))` pattern) and
20345        // TIMESTAMP + INTERVAL triplets (the Tier-A audit's
20346        // date-range iteration pattern, which pre-3.10 had no
20347        // direct equivalent in SPG).
20348        if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("generate_series"))
20349            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
20350        {
20351            self.advance(); // generate_series
20352            self.advance(); // (
20353            let mut args: Vec<Expr> = Vec::new();
20354            loop {
20355                args.push(self.parse_expr(0)?);
20356                if matches!(self.peek(), Token::Comma) {
20357                    self.advance();
20358                    continue;
20359                }
20360                break;
20361            }
20362            if !matches!(self.peek(), Token::RParen) {
20363                return Err(self.err(alloc::format!(
20364                    "expected ')' after generate_series() arguments, got {:?}",
20365                    self.peek()
20366                )));
20367            }
20368            self.advance();
20369            if args.len() < 2 || args.len() > 3 {
20370                return Err(self.err(alloc::format!(
20371                    "generate_series() expects 2 or 3 arguments (start, stop [, step]); got {}",
20372                    args.len()
20373                )));
20374            }
20375            let with_ordinality = self.absorb_with_ordinality();
20376            let (alias_ident, column_aliases) = self.parse_optional_alias_with_columns()?;
20377            let name = alias_ident
20378                .clone()
20379                .unwrap_or_else(|| "generate_series".to_string());
20380            let correlated = args.iter().any(Self::expr_has_any_column);
20381            let tref = TableRef {
20382                name,
20383                alias: alias_ident,
20384                only: false,
20385                as_of_segment: None,
20386                unnest_expr: None,
20387                unnest_column_aliases: column_aliases,
20388                with_ordinality,
20389                generate_series_args: Some(args),
20390                lateral_subquery: None,
20391                jsonb_each_text_arg: None,
20392                table_fn_call: None,
20393                rows_from: None,
20394                json_table: None,
20395                scalar_fn_item: false,
20396            };
20397            return Ok(if correlated {
20398                Self::wrap_correlated_srf(tref)
20399            } else {
20400                tref
20401            });
20402        }
20403        // v7.16.2 — preserve information_schema / pg_catalog
20404        // qualifiers (mailrs round-10 A.3). The generic
20405        // `expect_ident_like` strip silently drops the schema;
20406        // we want the engine to recognise these PG meta tables
20407        // and synthesise rows from the live catalog. Produce a
20408        // synthetic name (`__spg_info_columns` etc.) so the
20409        // engine's SELECT-side router can dispatch without
20410        // clashing with any user-defined `columns` table.
20411        let (name, meta_original) = if let Some((synth, orig)) = self.try_peek_meta_qualified() {
20412            (synth, Some(orig))
20413        } else if let Some((synth, orig)) = self.try_peek_meta_bare() {
20414            (synth, Some(orig))
20415        } else {
20416            (self.expect_ident_like()?, None)
20417        };
20418        // v6.10.2 — optional `AS OF SEGMENT '<id>'` cold-tier
20419        // time-travel clause. Parse BEFORE the alias so the
20420        // alias can still ride at the tail (`tbl AS OF SEGMENT
20421        // '5' alias`). `AS` is a reserved keyword token, while
20422        // `OF` and `SEGMENT` are bare idents.
20423        let as_of_segment = if matches!(self.peek(), Token::As)
20424            && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(s) | Token::QuotedIdent(s)) if s.eq_ignore_ascii_case("of"))
20425        {
20426            self.advance(); // AS
20427            self.advance(); // OF
20428            let kw = match self.peek().clone() {
20429                Token::Ident(s) | Token::QuotedIdent(s) => s,
20430                other => {
20431                    return Err(self.err(format!("expected SEGMENT after AS OF, got {other:?}")));
20432                }
20433            };
20434            if !kw.eq_ignore_ascii_case("segment") {
20435                return Err(self.err(format!(
20436                    "expected SEGMENT after AS OF, got {kw:?}; v6.10.2 supports SEGMENT only"
20437                )));
20438            }
20439            self.advance();
20440            // Segment id literal — accept either a string or
20441            // integer for operator ergonomics.
20442            let id = match self.advance() {
20443                Token::String(s) => s
20444                    .parse::<u32>()
20445                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20446                Token::Integer(n) => u32::try_from(n)
20447                    .map_err(|e| self.err(format!("AS OF SEGMENT id parse: {e}")))?,
20448                other => {
20449                    return Err(self.err(format!(
20450                        "expected segment id literal after AS OF SEGMENT, got {other:?}"
20451                    )));
20452                }
20453            };
20454            Some(id)
20455        } else {
20456            None
20457        };
20458        // TABLESAMPLE is not a reserved token — keep the bare-ident
20459        // alias rule from swallowing it (`FROM t TABLESAMPLE …`).
20460        let alias = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample"))
20461        {
20462            None
20463        } else {
20464            self.parse_optional_alias()?
20465        };
20466        // r1052 — a catalog name rewritten to its synthetic form keeps
20467        // the WRITTEN name as the relation's alias, so `pg_cast.oid`
20468        // still binds after `pg_cast` became `__spg_pg_cast`. PG
20469        // semantics: the visible name of `pg_catalog.pg_cast` IS
20470        // `pg_cast`. Without this, every table-name-qualified column
20471        // on a synthesised catalog answered "missing FROM-clause
20472        // entry" — which is the wall pg_dump hit on its first
20473        // pg_proc/pg_cast query.
20474        let alias = match (&alias, &meta_original) {
20475            (None, Some(orig)) if *orig != name => Some(orig.clone()),
20476            _ => alias,
20477        };
20478        // `TABLESAMPLE BERNOULLI(p) | SYSTEM(p)` follows the alias
20479        // (PG grammar). BERNOULLI lowers to a per-row
20480        // `random() < p/100` conjunct on the enclosing SELECT's
20481        // WHERE — exact row-level Bernoulli semantics. SYSTEM
20482        // shares the lowering: SPG has no page structure to
20483        // sample, and the row-level form returns the same expected
20484        // fraction. REPEATABLE(seed) promises a deterministic
20485        // sample SPG cannot honour yet — honest error rather than
20486        // a silently ignored seed.
20487        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("tablesample")) {
20488            self.advance();
20489            let method = self.expect_ident_like()?;
20490            if !method.eq_ignore_ascii_case("bernoulli") && !method.eq_ignore_ascii_case("system") {
20491                return Err(self.err(alloc::format!(
20492                    "TABLESAMPLE method {method:?} not supported; use BERNOULLI or SYSTEM"
20493                )));
20494            }
20495            if !matches!(self.peek(), Token::LParen) {
20496                return Err(self.err(alloc::format!(
20497                    "expected '(' after TABLESAMPLE {}, got {:?}",
20498                    method.to_ascii_uppercase(),
20499                    self.peek()
20500                )));
20501            }
20502            self.advance();
20503            let percent = self.parse_expr(0)?;
20504            if !matches!(self.peek(), Token::RParen) {
20505                return Err(self.err(alloc::format!(
20506                    "expected ')' after TABLESAMPLE percentage, got {:?}",
20507                    self.peek()
20508                )));
20509            }
20510            self.advance();
20511            // REPEATABLE(seed) → a deterministic per-row draw seeded by
20512            // `seed`, so the sample is stable across repeats and rescans.
20513            // Non-REPEATABLE keeps the non-deterministic `random()` draw.
20514            let mut sample_seed: Option<Expr> = None;
20515            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("repeatable")) {
20516                self.advance();
20517                if !matches!(self.peek(), Token::LParen) {
20518                    return Err(self.err(alloc::format!(
20519                        "expected '(' after REPEATABLE, got {:?}",
20520                        self.peek()
20521                    )));
20522                }
20523                self.advance();
20524                let seed = self.parse_expr(0)?;
20525                if !matches!(self.peek(), Token::RParen) {
20526                    return Err(self.err(alloc::format!(
20527                        "expected ')' after REPEATABLE seed, got {:?}",
20528                        self.peek()
20529                    )));
20530                }
20531                self.advance();
20532                sample_seed = Some(seed);
20533            }
20534            let draw = match sample_seed {
20535                Some(seed) => Expr::FunctionCall {
20536                    name: "__tsm_fract".to_string(),
20537                    args: alloc::vec![seed],
20538                },
20539                None => Expr::FunctionCall {
20540                    name: "random".to_string(),
20541                    args: Vec::new(),
20542                },
20543            };
20544            self.pending_sample_preds.push(Expr::Binary {
20545                lhs: Box::new(draw),
20546                op: crate::ast::BinOp::Lt,
20547                rhs: Box::new(Expr::Binary {
20548                    lhs: Box::new(percent),
20549                    op: crate::ast::BinOp::Div,
20550                    rhs: Box::new(Expr::Literal(crate::ast::Literal::Float(100.0))),
20551                }),
20552            });
20553        }
20554        Ok(TableRef {
20555            name,
20556            alias,
20557            only,
20558            as_of_segment,
20559            unnest_expr: None,
20560            unnest_column_aliases: Vec::new(),
20561            with_ordinality: false,
20562            generate_series_args: None,
20563            lateral_subquery: None,
20564            jsonb_each_text_arg: None,
20565            table_fn_call: None,
20566            rows_from: None,
20567            json_table: None,
20568            scalar_fn_item: false,
20569        })
20570    }
20571
20572    /// v7.13.2 — mailrs round-6 S5. Like `parse_optional_alias`
20573    /// but also accepts `AS alias(col [, col, …])` — the
20574    /// PG-standard table-function column-list form. The column
20575    /// list is only honoured when paired with `UNNEST(...)` in
20576    /// the parent; other call sites currently discard it.
20577    /// True when the expression tree contains a qualified column
20578    /// reference (`t.col`) — the syntactic marker that an SRF
20579    /// argument correlates with a preceding FROM item.
20580    fn expr_has_qualified_column(e: &Expr) -> bool {
20581        match e {
20582            Expr::Column(c) => c.qualifier.is_some(),
20583            Expr::Binary { lhs, rhs, .. } => {
20584                Self::expr_has_qualified_column(lhs) || Self::expr_has_qualified_column(rhs)
20585            }
20586            Expr::Unary { expr, .. } => Self::expr_has_qualified_column(expr),
20587            Expr::Cast { expr, .. } => Self::expr_has_qualified_column(expr),
20588            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_qualified_column),
20589            Expr::Case {
20590                operand,
20591                branches,
20592                else_branch,
20593            } => {
20594                operand
20595                    .as_deref()
20596                    .is_some_and(Self::expr_has_qualified_column)
20597                    || branches.iter().any(|(w, t)| {
20598                        Self::expr_has_qualified_column(w) || Self::expr_has_qualified_column(t)
20599                    })
20600                    || else_branch
20601                        .as_deref()
20602                        .is_some_and(Self::expr_has_qualified_column)
20603            }
20604            _ => false,
20605        }
20606    }
20607
20608    /// v7.38 (read01, T-lateral) — like `expr_has_qualified_column` but also
20609    /// counts a bare (unqualified) column. A set-returning function has no
20610    /// input columns of its own, so ANY column in its arguments is an outer
20611    /// (correlated) reference — `generate_series(1, n)` correlates on `n`.
20612    fn expr_has_any_column(e: &Expr) -> bool {
20613        match e {
20614            Expr::Column(_) => true,
20615            Expr::Binary { lhs, rhs, .. } => {
20616                Self::expr_has_any_column(lhs) || Self::expr_has_any_column(rhs)
20617            }
20618            Expr::Unary { expr, .. } => Self::expr_has_any_column(expr),
20619            Expr::Cast { expr, .. } => Self::expr_has_any_column(expr),
20620            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_any_column),
20621            // v7.39 (round 759, F31-B8b) — a column INSIDE an array
20622            // constructor or subscript fell to the `_ => false` arm, so
20623            // `unnest(ARRAY[x, x + 1])` never wrapped into the lateral
20624            // channel and the eager peer eval answered `column "x" does
20625            // not exist` (the substitution walker already recurses both
20626            // shapes; only this detector was blind to them).
20627            Expr::Array(items) => items.iter().any(Self::expr_has_any_column),
20628            Expr::ArraySubscript { target, index } => {
20629                Self::expr_has_any_column(target) || Self::expr_has_any_column(index)
20630            }
20631            Expr::Case {
20632                operand,
20633                branches,
20634                else_branch,
20635            } => {
20636                operand.as_deref().is_some_and(Self::expr_has_any_column)
20637                    || branches
20638                        .iter()
20639                        .any(|(w, t)| Self::expr_has_any_column(w) || Self::expr_has_any_column(t))
20640                    || else_branch
20641                        .as_deref()
20642                        .is_some_and(Self::expr_has_any_column)
20643            }
20644            _ => false,
20645        }
20646    }
20647
20648    /// Wrap a correlated SRF table ref (`unnest(t.col)` /
20649    /// `generate_series(1, t.n)`) into the lateral_subquery
20650    /// channel: `SELECT * FROM <srf>` executes per outer row with
20651    /// outer references substituted (v7.37.43-T4.5 machinery).
20652    /// Uncorrelated SRFs stay on their plain channels.
20653    fn wrap_correlated_srf(srf: TableRef) -> TableRef {
20654        let name = srf.name.clone();
20655        let alias = srf.alias.clone();
20656        let inner = crate::ast::SelectStatement {
20657            locking: None,
20658            ctes: Vec::new(),
20659            distinct: false,
20660            distinct_on: Vec::new(),
20661            items: alloc::vec![crate::ast::SelectItem::Wildcard],
20662            from: Some(crate::ast::FromClause {
20663                primary: srf,
20664                joins: Vec::new(),
20665            }),
20666            where_: None,
20667            group_by: None,
20668            group_by_all: false,
20669            having: None,
20670            unions: Vec::new(),
20671            order_by: Vec::new(),
20672            limit: None,
20673            offset: None,
20674            limit_with_ties: false,
20675            window_check_exprs: Vec::new(),
20676        };
20677        TableRef {
20678            name,
20679            alias,
20680            only: false,
20681            as_of_segment: None,
20682            unnest_expr: None,
20683            unnest_column_aliases: Vec::new(),
20684            with_ordinality: false,
20685            generate_series_args: None,
20686            lateral_subquery: Some(Box::new(inner)),
20687            jsonb_each_text_arg: None,
20688            table_fn_call: None,
20689            rows_from: None,
20690            json_table: None,
20691            scalar_fn_item: false,
20692        }
20693    }
20694
20695    /// True when the expression tree contains an unresolved
20696    /// `OVER w` marker (see parse_over_clause).
20697    fn expr_has_named_window(e: &Expr) -> bool {
20698        match e {
20699            Expr::WindowFunction { partition_by, .. } => matches!(
20700                partition_by.as_slice(),
20701                [Expr::Column(c)] if matches!(
20702                    c.qualifier.as_deref(),
20703                    Some("__named_window__") | Some("__named_window_ref__")
20704                )
20705            ),
20706            Expr::Binary { lhs, rhs, .. } => {
20707                Self::expr_has_named_window(lhs) || Self::expr_has_named_window(rhs)
20708            }
20709            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => Self::expr_has_named_window(expr),
20710            Expr::FunctionCall { args, .. } => args.iter().any(Self::expr_has_named_window),
20711            Expr::Case {
20712                operand,
20713                branches,
20714                else_branch,
20715            } => {
20716                operand.as_deref().is_some_and(Self::expr_has_named_window)
20717                    || branches.iter().any(|(w, t)| {
20718                        Self::expr_has_named_window(w) || Self::expr_has_named_window(t)
20719                    })
20720                    || else_branch
20721                        .as_deref()
20722                        .is_some_and(Self::expr_has_named_window)
20723            }
20724            _ => false,
20725        }
20726    }
20727
20728    /// v7.39 (round 705) — the NAMES the expression references through the
20729    /// `OVER w` markers, so `parse_bare_select` can tell which WINDOW
20730    /// definitions nothing referenced. Traversal mirrors
20731    /// `expr_has_named_window` above.
20732    fn collect_named_window_refs(e: &Expr, into: &mut Vec<String>) {
20733        match e {
20734            Expr::WindowFunction { partition_by, .. } => {
20735                if let [Expr::Column(c)] = partition_by.as_slice()
20736                    && matches!(
20737                        c.qualifier.as_deref(),
20738                        Some("__named_window__") | Some("__named_window_ref__")
20739                    )
20740                {
20741                    into.push(c.name.clone());
20742                }
20743            }
20744            Expr::Binary { lhs, rhs, .. } => {
20745                Self::collect_named_window_refs(lhs, into);
20746                Self::collect_named_window_refs(rhs, into);
20747            }
20748            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20749                Self::collect_named_window_refs(expr, into);
20750            }
20751            Expr::FunctionCall { args, .. } => {
20752                for a in args {
20753                    Self::collect_named_window_refs(a, into);
20754                }
20755            }
20756            Expr::Case {
20757                operand,
20758                branches,
20759                else_branch,
20760            } => {
20761                if let Some(o) = operand.as_deref() {
20762                    Self::collect_named_window_refs(o, into);
20763                }
20764                for (w, t) in branches {
20765                    Self::collect_named_window_refs(w, into);
20766                    Self::collect_named_window_refs(t, into);
20767                }
20768                if let Some(eb) = else_branch.as_deref() {
20769                    Self::collect_named_window_refs(eb, into);
20770                }
20771            }
20772            _ => {}
20773        }
20774    }
20775
20776    /// Inline named-window definitions into the `OVER w` markers.
20777    /// An unknown name errors (PG: window "w" does not exist).
20778    #[allow(clippy::type_complexity)]
20779    fn substitute_named_windows(
20780        e: &mut Expr,
20781        defs: &[(
20782            String,
20783            (
20784                Vec<Expr>,
20785                Vec<(Expr, bool, Option<bool>)>,
20786                Option<WindowFrame>,
20787            ),
20788        )],
20789    ) -> Result<(), String> {
20790        match e {
20791            Expr::WindowFunction {
20792                partition_by,
20793                order_by,
20794                frame,
20795                ..
20796            } => {
20797                // `is_copy` distinguishes `OVER (w1 …)` (a refinable copy)
20798                // from the bare `OVER w1` (a plain reference).
20799                let named = match partition_by.as_slice() {
20800                    [Expr::Column(c)] => match c.qualifier.as_deref() {
20801                        Some("__named_window__") => Some((c.name.clone(), false)),
20802                        Some("__named_window_ref__") => Some((c.name.clone(), true)),
20803                        _ => None,
20804                    },
20805                    _ => None,
20806                };
20807                if let Some((wname, is_copy)) = named {
20808                    let Some((_, def)) = defs.iter().find(|(n, _)| n.eq_ignore_ascii_case(&wname))
20809                    else {
20810                        return Err(alloc::format!("window {wname:?} does not exist"));
20811                    };
20812                    if !is_copy {
20813                        *partition_by = def.0.clone();
20814                        *order_by = def.1.clone();
20815                        *frame = def.2.clone();
20816                        return Ok(());
20817                    }
20818                    // v7.39 (round 229) — PG's copy rules, probed against
20819                    // 18.4: a copy inherits the partitioning, may supply an
20820                    // ordering only when the base has none, and may not copy
20821                    // a base that already carries a frame (its own frame
20822                    // would be ambiguous with the inherited one).
20823                    if !def.1.is_empty() && !order_by.is_empty() {
20824                        return Err(alloc::format!(
20825                            "cannot override ORDER BY clause of window \"{wname}\""
20826                        ));
20827                    }
20828                    if def.2.is_some() {
20829                        return Err(alloc::format!(
20830                            "cannot copy window \"{wname}\" because it has a frame clause"
20831                        ));
20832                    }
20833                    *partition_by = def.0.clone();
20834                    if order_by.is_empty() {
20835                        *order_by = def.1.clone();
20836                    }
20837                }
20838                Ok(())
20839            }
20840            Expr::Binary { lhs, rhs, .. } => {
20841                Self::substitute_named_windows(lhs, defs)?;
20842                Self::substitute_named_windows(rhs, defs)
20843            }
20844            Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => {
20845                Self::substitute_named_windows(expr, defs)
20846            }
20847            Expr::FunctionCall { args, .. } => {
20848                for a in args {
20849                    Self::substitute_named_windows(a, defs)?;
20850                }
20851                Ok(())
20852            }
20853            Expr::Case {
20854                operand,
20855                branches,
20856                else_branch,
20857            } => {
20858                if let Some(op) = operand {
20859                    Self::substitute_named_windows(op, defs)?;
20860                }
20861                for (w, t) in branches {
20862                    Self::substitute_named_windows(w, defs)?;
20863                    Self::substitute_named_windows(t, defs)?;
20864                }
20865                if let Some(el) = else_branch {
20866                    Self::substitute_named_windows(el, defs)?;
20867                }
20868                Ok(())
20869            }
20870            _ => Ok(()),
20871        }
20872    }
20873
20874    /// SQL-standard `TABLE name` shorthand — builds the equivalent
20875    /// `SELECT * FROM name` head. Callers own set-op chain / tail
20876    /// composition.
20877    fn parse_table_shorthand(&mut self) -> Result<SelectStatement, ParseError> {
20878        debug_assert!(matches!(self.peek(), Token::Table));
20879        self.advance(); // TABLE
20880        let tname = self.expect_ident_like()?;
20881        Ok(SelectStatement {
20882            locking: None,
20883            ctes: Vec::new(),
20884            distinct: false,
20885            distinct_on: Vec::new(),
20886            items: alloc::vec![SelectItem::Wildcard],
20887            from: Some(FromClause {
20888                primary: TableRef {
20889                    name: tname,
20890                    alias: None,
20891                    only: false,
20892                    as_of_segment: None,
20893                    unnest_expr: None,
20894                    unnest_column_aliases: Vec::new(),
20895                    with_ordinality: false,
20896                    generate_series_args: None,
20897                    lateral_subquery: None,
20898                    jsonb_each_text_arg: None,
20899                    table_fn_call: None,
20900                    rows_from: None,
20901                    json_table: None,
20902                    scalar_fn_item: false,
20903                },
20904                joins: Vec::new(),
20905            }),
20906            where_: None,
20907            group_by: None,
20908            group_by_all: false,
20909            having: None,
20910            unions: Vec::new(),
20911            order_by: Vec::new(),
20912            limit: None,
20913            offset: None,
20914            limit_with_ties: false,
20915            window_check_exprs: Vec::new(),
20916        })
20917    }
20918
20919    /// `jsonb_to_recordset(J) AS t(c1 t1, c2 t2, …)` (and record / json_
20920    /// variants) → a derived table that reads each declared column out of
20921    /// the JSON with `(row ->> 'ci')::ti`. The *set form iterates
20922    /// `jsonb_array_elements(J)` (one row per element, column `value`);
20923    /// the scalar *record form projects a single row straight off `J`.
20924    /// Rides the existing lateral-subquery channel, so no new executor or
20925    /// AST is needed.
20926    fn parse_json_to_record_from(&mut self) -> Result<TableRef, ParseError> {
20927        use crate::ast::{
20928            BinOp, ColumnName, Expr, FromClause, Literal, SelectItem, SelectStatement,
20929        };
20930        let fn_name = match self.peek() {
20931            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
20932            _ => unreachable!("caller guarded is_json_to_record_name"),
20933        };
20934        self.advance(); // fn name
20935        self.advance(); // (
20936        let mut arg = self.parse_expr(0)?;
20937        // populate_record(base, json): the base only carries the record
20938        // type here — the JSON argument is the second expression.
20939        let mut base: Option<Expr> = None;
20940        if matches!(self.peek(), Token::Comma) {
20941            self.advance();
20942            base = Some(arg);
20943            arg = self.parse_expr(0)?;
20944        }
20945        if !matches!(self.peek(), Token::RParen) {
20946            return Err(self.err(alloc::format!(
20947                "expected ')' after {fn_name}() argument, got {:?}",
20948                self.peek()
20949            )));
20950        }
20951        self.advance(); // )
20952        let is_set = fn_name.ends_with("recordset");
20953        // `[AS] alias ( col type [, …] )` column-definition list.
20954        if matches!(self.peek(), Token::As) {
20955            self.advance();
20956        }
20957        let alias_opt = match self.peek() {
20958            Token::Ident(s) | Token::QuotedIdent(s) => {
20959                let a = s.clone();
20960                self.advance();
20961                Some(a)
20962            }
20963            _ => None,
20964        };
20965        // v7.39 (read01 round 76) — the populate family's canonical PG
20966        // spelling carries no column list at all: the row shape comes from
20967        // the BASE argument's declared type (`jsonb_populate_record(NULL::t,
20968        // j)`). The parser has no catalog, so hand the two arguments to the
20969        // engine's table-function channel, which does. Only `*_to_record*`
20970        // (whose base is bare `record`) genuinely requires the list.
20971        if !matches!(self.peek(), Token::LParen) {
20972            if let Some(base_expr) = base {
20973                let alias = alias_opt.unwrap_or_else(|| fn_name.clone());
20974                return Ok(TableRef {
20975                    name: alias.clone(),
20976                    alias: Some(alias),
20977                    only: false,
20978                    as_of_segment: None,
20979                    unnest_expr: None,
20980                    unnest_column_aliases: Vec::new(),
20981                    with_ordinality: false,
20982                    generate_series_args: None,
20983                    lateral_subquery: None,
20984                    jsonb_each_text_arg: None,
20985                    table_fn_call: Some(Box::new((fn_name, alloc::vec![base_expr, arg]))),
20986                    rows_from: None,
20987                    json_table: None,
20988                    scalar_fn_item: false,
20989                });
20990            }
20991            return Err(self.err(alloc::format!(
20992                "expected '(' to start the {fn_name} column-definition list, got {:?}",
20993                self.peek()
20994            )));
20995        }
20996        let Some(alias) = alias_opt else {
20997            return Err(self.err(alloc::format!(
20998                "{fn_name}(...) needs a column-definition list, e.g. AS t(a int, b text)"
20999            )));
21000        };
21001        self.advance(); // (
21002        let mut coldefs: Vec<(String, crate::ast::CastTarget)> = Vec::new();
21003        loop {
21004            let col = self.expect_ident_like()?;
21005            let ty = self.parse_cast_target()?;
21006            coldefs.push((col, ty));
21007            if matches!(self.peek(), Token::Comma) {
21008                self.advance();
21009                continue;
21010            }
21011            if matches!(self.peek(), Token::RParen) {
21012                self.advance();
21013                break;
21014            }
21015            return Err(self.err(alloc::format!(
21016                "expected ',' or ')' in {fn_name} column list, got {:?}",
21017                self.peek()
21018            )));
21019        }
21020        if coldefs.is_empty() {
21021            return Err(self.err(alloc::format!(
21022                "{fn_name} column-definition list must declare at least one column"
21023            )));
21024        }
21025        // Per column: (base ->> 'col')::type AS col. The base is the
21026        // per-element `value` column for the *set form, or the argument
21027        // itself for the scalar record form.
21028        let items: Vec<SelectItem> = coldefs
21029            .into_iter()
21030            .map(|(col, ty)| {
21031                let base = if is_set {
21032                    Expr::Column(ColumnName {
21033                        qualifier: None,
21034                        name: "value".to_string(),
21035                    })
21036                } else {
21037                    arg.clone()
21038                };
21039                SelectItem::Expr {
21040                    expr: Expr::Cast {
21041                        expr: Box::new(Expr::Binary {
21042                            lhs: Box::new(base),
21043                            op: BinOp::JsonGetText,
21044                            rhs: Box::new(Expr::Literal(Literal::String(col.clone()))),
21045                        }),
21046                        target: ty,
21047                    },
21048                    alias: Some(col),
21049                }
21050            })
21051            .collect();
21052        let from = if is_set {
21053            let elem_fn = if fn_name.starts_with("jsonb") {
21054                "jsonb_array_elements"
21055            } else {
21056                "json_array_elements"
21057            };
21058            Some(FromClause {
21059                primary: TableRef {
21060                    name: "value".to_string(),
21061                    alias: None,
21062                    only: false,
21063                    as_of_segment: None,
21064                    unnest_expr: Some(Box::new(Expr::FunctionCall {
21065                        name: elem_fn.to_string(),
21066                        args: alloc::vec![arg],
21067                    })),
21068                    unnest_column_aliases: alloc::vec!["value".to_string()],
21069                    with_ordinality: false,
21070                    generate_series_args: None,
21071                    lateral_subquery: None,
21072                    jsonb_each_text_arg: None,
21073                    table_fn_call: None,
21074                    rows_from: None,
21075                    json_table: None,
21076                    scalar_fn_item: false,
21077                },
21078                joins: Vec::new(),
21079            })
21080        } else {
21081            None
21082        };
21083        let inner = SelectStatement {
21084            locking: None,
21085            ctes: Vec::new(),
21086            distinct: false,
21087            distinct_on: Vec::new(),
21088            items,
21089            from,
21090            where_: None,
21091            group_by: None,
21092            group_by_all: false,
21093            having: None,
21094            unions: Vec::new(),
21095            order_by: Vec::new(),
21096            limit: None,
21097            offset: None,
21098            limit_with_ties: false,
21099            window_check_exprs: Vec::new(),
21100        };
21101        Ok(TableRef {
21102            name: alias.clone(),
21103            alias: Some(alias),
21104            only: false,
21105            as_of_segment: None,
21106            unnest_expr: None,
21107            unnest_column_aliases: Vec::new(),
21108            with_ordinality: false,
21109            generate_series_args: None,
21110            lateral_subquery: Some(Box::new(inner)),
21111            jsonb_each_text_arg: None,
21112            table_fn_call: None,
21113            rows_from: None,
21114            json_table: None,
21115            scalar_fn_item: false,
21116        })
21117    }
21118
21119    /// Absorb `WITH ORDINALITY` after an SRF call in FROM position.
21120    /// Returns true when the clause was present. `WITH` alone (a
21121    /// CTE can never start here) is not enough — the ORDINALITY
21122    /// ident must follow, so a stray WITH still errors downstream.
21123    fn absorb_with_ordinality(&mut self) -> bool {
21124        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
21125            && matches!(self.tokens.get(self.pos + 1),
21126                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ordinality"))
21127        {
21128            self.advance();
21129            self.advance();
21130            true
21131        } else {
21132            false
21133        }
21134    }
21135
21136    /// v7.39 (read01 partitionfuncs.c) — parse a FROM-position table
21137    /// function reference (`pg_partition_tree('t') [AS a(c, …)]`).
21138    /// Out-of-line: the caller sits on the FROM recursion chain.
21139    #[inline(never)]
21140    fn parse_table_fn_ref(&mut self) -> Result<TableRef, ParseError> {
21141        let fn_name = match self.advance() {
21142            Token::Ident(s) | Token::QuotedIdent(s) => s.to_ascii_lowercase(),
21143            _ => unreachable!("caller peeked an ident"),
21144        };
21145        self.advance(); // (
21146        let mut args: Vec<Expr> = Vec::new();
21147        if !matches!(self.peek(), Token::RParen) {
21148            loop {
21149                args.push(self.parse_expr(0)?);
21150                if matches!(self.peek(), Token::Comma) {
21151                    self.advance();
21152                    continue;
21153                }
21154                break;
21155            }
21156        }
21157        if !matches!(self.peek(), Token::RParen) {
21158            return Err(self.err(alloc::format!(
21159                "expected ')' after {fn_name}() arguments, got {:?}",
21160                self.peek()
21161            )));
21162        }
21163        self.advance();
21164        // v7.39 (read01 round 68) — `f(args) WITH ORDINALITY AS a(x, n)`: the
21165        // counter column rides after the function's own, and the alias list
21166        // names it.
21167        let with_ordinality = self.absorb_with_ordinality();
21168        let (alias_ident, unnest_column_aliases) = self.parse_optional_alias_with_columns()?;
21169        let name = alias_ident.clone().unwrap_or_else(|| fn_name.clone());
21170        Ok(TableRef {
21171            name,
21172            alias: alias_ident,
21173            only: false,
21174            as_of_segment: None,
21175            unnest_expr: None,
21176            unnest_column_aliases,
21177            with_ordinality,
21178            generate_series_args: None,
21179            lateral_subquery: None,
21180            jsonb_each_text_arg: None,
21181            table_fn_call: Some(Box::new((fn_name, args))),
21182            rows_from: None,
21183            json_table: None,
21184            scalar_fn_item: false,
21185        })
21186    }
21187
21188    /// v7.39 (round 205, JSON_TABLE) — parse
21189    /// `JSON_TABLE(<doc>, '<row_path>' [PASSING …] COLUMNS (<coldefs>))
21190    /// [AS <alias>]`. The COLUMNS list is a recursive tree (NESTED
21191    /// PATH nests another COLUMNS). Out-of-line (FROM recursion chain).
21192    #[inline(never)]
21193    fn parse_json_table_ref(&mut self) -> Result<TableRef, ParseError> {
21194        self.advance(); // json_table
21195        self.advance(); // (
21196        let doc = Box::new(self.parse_expr(0)?);
21197        self.expect_comma_json_table()?;
21198        let row_path = self.parse_json_string_literal("JSON_TABLE row path")?;
21199        // Optional `PASSING <expr> AS <name> [, …]`.
21200        let mut passing: Vec<(String, Expr)> = Vec::new();
21201        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("passing")) {
21202            self.advance();
21203            loop {
21204                let e = self.parse_expr(0)?;
21205                if !matches!(self.peek(), Token::As) {
21206                    return Err(self.err("expected AS after JSON_TABLE PASSING value".into()));
21207                }
21208                self.advance();
21209                let vname = match self.advance() {
21210                    Token::Ident(s) | Token::QuotedIdent(s) => s,
21211                    other => {
21212                        return Err(self.err(alloc::format!(
21213                            "expected PASSING variable name, got {other:?}"
21214                        )));
21215                    }
21216                };
21217                passing.push((vname, e));
21218                if matches!(self.peek(), Token::Comma) {
21219                    self.advance();
21220                    continue;
21221                }
21222                break;
21223            }
21224        }
21225        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21226            return Err(self.err("expected COLUMNS in JSON_TABLE".into()));
21227        }
21228        self.advance();
21229        let columns = self.parse_json_table_columns()?;
21230        if !matches!(self.peek(), Token::RParen) {
21231            return Err(self.err(alloc::format!(
21232                "expected ')' to close JSON_TABLE, got {:?}",
21233                self.peek()
21234            )));
21235        }
21236        self.advance();
21237        let alias_ident = self.parse_optional_alias()?;
21238        let name = alias_ident
21239            .clone()
21240            .unwrap_or_else(|| String::from("json_table"));
21241        Ok(TableRef {
21242            name,
21243            alias: alias_ident,
21244            only: false,
21245            as_of_segment: None,
21246            unnest_expr: None,
21247            unnest_column_aliases: Vec::new(),
21248            with_ordinality: false,
21249            generate_series_args: None,
21250            lateral_subquery: None,
21251            jsonb_each_text_arg: None,
21252            table_fn_call: None,
21253            rows_from: None,
21254            json_table: Some(Box::new(crate::ast::JsonTable {
21255                doc,
21256                row_path,
21257                columns,
21258                passing,
21259            })),
21260            scalar_fn_item: false,
21261        })
21262    }
21263
21264    fn expect_comma_json_table(&mut self) -> Result<(), ParseError> {
21265        if !matches!(self.peek(), Token::Comma) {
21266            return Err(self.err(alloc::format!(
21267                "expected ',' after JSON_TABLE document, got {:?}",
21268                self.peek()
21269            )));
21270        }
21271        self.advance();
21272        Ok(())
21273    }
21274
21275    fn parse_json_string_literal(&mut self, what: &str) -> Result<String, ParseError> {
21276        match self.advance() {
21277            Token::String(s) => Ok(s),
21278            other => Err(self.err(alloc::format!(
21279                "expected {what} string literal, got {other:?}"
21280            ))),
21281        }
21282    }
21283
21284    /// v7.39 (round 205) — `( <coldef> [, <coldef>]* )`.
21285    #[inline(never)]
21286    fn parse_json_table_columns(
21287        &mut self,
21288    ) -> Result<alloc::vec::Vec<crate::ast::JsonTableColumn>, ParseError> {
21289        if !matches!(self.peek(), Token::LParen) {
21290            return Err(self.err("expected '(' after COLUMNS".into()));
21291        }
21292        self.advance();
21293        let mut cols = Vec::new();
21294        loop {
21295            cols.push(self.parse_json_table_one_column()?);
21296            if matches!(self.peek(), Token::Comma) {
21297                self.advance();
21298                continue;
21299            }
21300            break;
21301        }
21302        if !matches!(self.peek(), Token::RParen) {
21303            return Err(self.err(alloc::format!(
21304                "expected ')' after JSON_TABLE COLUMNS, got {:?}",
21305                self.peek()
21306            )));
21307        }
21308        self.advance();
21309        Ok(cols)
21310    }
21311
21312    fn parse_json_table_one_column(&mut self) -> Result<crate::ast::JsonTableColumn, ParseError> {
21313        use crate::ast::{JsonTableColumn, JsonTableOnBehavior};
21314        // NESTED [PATH] '<p>' COLUMNS (...)
21315        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("nested")) {
21316            self.advance();
21317            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21318                self.advance();
21319            }
21320            let path = self.parse_json_string_literal("NESTED PATH")?;
21321            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("columns")) {
21322                return Err(self.err("expected COLUMNS after NESTED PATH".into()));
21323            }
21324            self.advance();
21325            let columns = self.parse_json_table_columns()?;
21326            return Ok(JsonTableColumn::Nested { path, columns });
21327        }
21328        // <name> ...
21329        let name = match self.advance() {
21330            Token::Ident(s) | Token::QuotedIdent(s) => s,
21331            other => {
21332                return Err(self.err(alloc::format!("expected column name, got {other:?}")));
21333            }
21334        };
21335        // <name> FOR ORDINALITY
21336        if matches!(self.peek(), Token::For) {
21337            self.advance();
21338            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ordinality")) {
21339                return Err(self.err("expected ORDINALITY after FOR".into()));
21340            }
21341            self.advance();
21342            return Ok(JsonTableColumn::Ordinality { name });
21343        }
21344        // <name> <type> [FORMAT JSON] {PATH '<p>' | EXISTS [PATH '<p>']} [WITH WRAPPER] [ON …]
21345        let ty = self.parse_column_type_name()?;
21346        let mut format_json = false;
21347        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21348            self.advance();
21349            if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21350                return Err(self.err("expected JSON after FORMAT".into()));
21351            }
21352            self.advance();
21353            format_json = true;
21354        }
21355        let mut exists = false;
21356        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exists")) {
21357            self.advance();
21358            exists = true;
21359        }
21360        let mut path = alloc::format!("$.{name}");
21361        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("path")) {
21362            self.advance();
21363            path = self.parse_json_string_literal("column PATH")?;
21364        }
21365        if !exists && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("format")) {
21366            // `FORMAT JSON` after PATH (alternate placement).
21367            self.advance();
21368            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("json")) {
21369                self.advance();
21370            }
21371            format_json = true;
21372        }
21373        let mut wrapper = false;
21374        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with")) {
21375            self.advance();
21376            // optional CONDITIONAL/UNCONDITIONAL
21377            if matches!(self.peek(), Token::Ident(s)
21378                if s.eq_ignore_ascii_case("unconditional")
21379                    || s.eq_ignore_ascii_case("conditional"))
21380            {
21381                self.advance();
21382            }
21383            if !matches!(self.peek(), Token::Ident(s)
21384                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21385            {
21386                return Err(self.err("expected WRAPPER after WITH".into()));
21387            }
21388            self.advance();
21389            // optional `ARRAY` after `WRAPPER`, or `WRAPPER` after `ARRAY`
21390            if matches!(self.peek(), Token::Ident(s)
21391                if s.eq_ignore_ascii_case("wrapper") || s.eq_ignore_ascii_case("array"))
21392            {
21393                self.advance();
21394            }
21395            wrapper = true;
21396        }
21397        // ON EMPTY / ON ERROR clauses (two, in any order).
21398        let mut on_empty = JsonTableOnBehavior::Null;
21399        let mut on_error = JsonTableOnBehavior::Null;
21400        for _ in 0..2 {
21401            let behavior = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error"))
21402            {
21403                self.advance();
21404                Some(JsonTableOnBehavior::Error)
21405            } else if matches!(self.peek(), Token::Null) {
21406                self.advance();
21407                Some(JsonTableOnBehavior::Null)
21408            } else if matches!(self.peek(), Token::Default) {
21409                self.advance();
21410                Some(JsonTableOnBehavior::Default(Box::new(self.parse_expr(0)?)))
21411            } else {
21412                None
21413            };
21414            let Some(behavior) = behavior else { break };
21415            // `ON {EMPTY|ERROR}`
21416            if !matches!(self.peek(), Token::On) {
21417                return Err(self.err("expected ON after JSON_TABLE column behavior".into()));
21418            }
21419            self.advance();
21420            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("empty")) {
21421                self.advance();
21422                on_empty = behavior;
21423            } else if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("error")) {
21424                self.advance();
21425                on_error = behavior;
21426            } else {
21427                return Err(self.err("expected EMPTY or ERROR after ON".into()));
21428            }
21429        }
21430        Ok(JsonTableColumn::Regular {
21431            name,
21432            ty,
21433            path,
21434            exists,
21435            format_json,
21436            wrapper,
21437            on_empty,
21438            on_error,
21439        })
21440    }
21441
21442    fn parse_optional_alias_with_columns(
21443        &mut self,
21444    ) -> Result<(Option<String>, Vec<String>), ParseError> {
21445        let alias = self.parse_optional_alias()?;
21446        if alias.is_none() {
21447            return Ok((None, Vec::new()));
21448        }
21449        let mut cols: Vec<String> = Vec::new();
21450        if matches!(self.peek(), Token::LParen) {
21451            self.advance();
21452            while let Token::Ident(s) | Token::QuotedIdent(s) = self.peek().clone() {
21453                self.advance();
21454                cols.push(s);
21455                if matches!(self.peek(), Token::Comma) {
21456                    self.advance();
21457                    continue;
21458                }
21459                break;
21460            }
21461            if matches!(self.peek(), Token::RParen) {
21462                self.advance();
21463            }
21464        }
21465        Ok((alias, cols))
21466    }
21467
21468    /// v7.37.16 — parse a `left(str, n)` / `right(str, n)` function call
21469    /// whose keyword token was already consumed and whose `(` is the
21470    /// current token. Factored out of `parse_atom` (and marked
21471    /// `#[inline(never)]`) so its `Vec`/loop locals stay OFF the giant
21472    /// recursive `parse_atom` frame — inlining them there enlarges the
21473    /// per-nesting-level stack cost that `MAX_NEST_DEPTH` is tuned
21474    /// against, risking an overflow before the budget triggers.
21475    #[inline(never)]
21476    fn parse_lr_string_function_call(&mut self, name: &str) -> Result<Expr, ParseError> {
21477        self.advance(); // (
21478        let mut args = Vec::new();
21479        if !matches!(self.peek(), Token::RParen) {
21480            loop {
21481                args.push(self.parse_expr(0)?);
21482                match self.peek() {
21483                    Token::Comma => {
21484                        self.advance();
21485                    }
21486                    Token::RParen => break,
21487                    other => {
21488                        return Err(self.err(alloc::format!(
21489                            "expected ',' or ')' in {name}() args, got {other:?}"
21490                        )));
21491                    }
21492                }
21493            }
21494        }
21495        self.advance(); // )
21496        Ok(Expr::FunctionCall {
21497            name: name.into(),
21498            args,
21499        })
21500    }
21501
21502    /// FROM-clause: a primary table reference plus zero-or-more joined
21503    /// peers expressed via either `, <table>` (cross-product, no ON) or
21504    /// `[INNER|LEFT|RIGHT [OUTER]|FULL [OUTER]|CROSS] JOIN <table> [ON expr]`.
21505    /// v1.10 keeps the join list flat (left-associative nested-loop
21506    /// semantics).
21507    fn parse_from_clause(&mut self) -> Result<FromClause, ParseError> {
21508        let primary = self.parse_table_ref()?;
21509        let primary_qual = primary
21510            .alias
21511            .clone()
21512            .unwrap_or_else(|| primary.name.clone());
21513        let joins = self.parse_from_joins(&primary_qual)?;
21514        Ok(FromClause { primary, joins })
21515    }
21516
21517    /// v7.39 (round 420) — the join tail of a FROM clause, factored out of
21518    /// [`Self::parse_from_clause`] so MySQL's multi-table UPDATE can read the
21519    /// SAME grammar after its target table has already been consumed.
21520    /// (`advance()` destroys the tokens it returns — `mem::replace(.., Eof)`
21521    /// — so re-parsing by rewinding `self.pos` is not possible; the tail must
21522    /// be parsed forward, once.)
21523    /// `left_primary_qual` is the qualifier (alias, else name) of whatever
21524    /// sits to the LEFT of the first join — the FROM primary, or the UPDATE
21525    /// target in the MySQL multi-table form. It only feeds the `USING (…)`
21526    /// desugaring, which needs a name for the left side of each equality.
21527    fn parse_from_joins(&mut self, left_primary_qual: &str) -> Result<Vec<FromJoin>, ParseError> {
21528        let mut joins = Vec::new();
21529        loop {
21530            // `, <table>` — cross-product with no ON.
21531            if matches!(self.peek(), Token::Comma) {
21532                self.advance();
21533                let table = self.parse_table_ref()?;
21534                joins.push(FromJoin {
21535                    kind: JoinKind::Cross,
21536                    table,
21537                    on: None,
21538                    using_cols: None,
21539                    natural: false,
21540                });
21541                continue;
21542            }
21543            // v7.37.16 — optional leading `NATURAL` before the join
21544            // kind: `NATURAL JOIN`, `NATURAL LEFT JOIN`, etc. NATURAL is
21545            // not a lexer keyword (it arrives as a bare Ident), so match
21546            // it case-insensitively here. When present, no ON/USING
21547            // clause is allowed — the common columns are resolved at
21548            // execution time.
21549            let natural = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("natural"));
21550            if natural {
21551                self.advance();
21552            }
21553            // Explicit JOIN syntax. Accept INNER JOIN, LEFT [OUTER] JOIN,
21554            // CROSS JOIN, and bare JOIN (defaults to INNER).
21555            let kind =
21556                match self.peek() {
21557                    Token::Inner => {
21558                        self.advance();
21559                        if !matches!(self.peek(), Token::Join) {
21560                            return Err(self
21561                                .err(format!("expected JOIN after INNER, got {:?}", self.peek())));
21562                        }
21563                        self.advance();
21564                        JoinKind::Inner
21565                    }
21566                    Token::Left => {
21567                        self.advance();
21568                        if matches!(self.peek(), Token::Outer) {
21569                            self.advance();
21570                        }
21571                        if !matches!(self.peek(), Token::Join) {
21572                            return Err(self.err(format!(
21573                                "expected JOIN after LEFT [OUTER], got {:?}",
21574                                self.peek()
21575                            )));
21576                        }
21577                        self.advance();
21578                        JoinKind::Left
21579                    }
21580                    Token::Cross => {
21581                        self.advance();
21582                        if !matches!(self.peek(), Token::Join) {
21583                            return Err(self
21584                                .err(format!("expected JOIN after CROSS, got {:?}", self.peek())));
21585                        }
21586                        self.advance();
21587                        JoinKind::Cross
21588                    }
21589                    // v7.37.16 — RIGHT [OUTER] JOIN. OUTER is optional noise.
21590                    Token::Right => {
21591                        self.advance();
21592                        if matches!(self.peek(), Token::Outer) {
21593                            self.advance();
21594                        }
21595                        if !matches!(self.peek(), Token::Join) {
21596                            return Err(self.err(format!(
21597                                "expected JOIN after RIGHT [OUTER], got {:?}",
21598                                self.peek()
21599                            )));
21600                        }
21601                        self.advance();
21602                        JoinKind::Right
21603                    }
21604                    // v7.37.16 — FULL [OUTER] JOIN. OUTER is optional noise.
21605                    Token::Full => {
21606                        self.advance();
21607                        if matches!(self.peek(), Token::Outer) {
21608                            self.advance();
21609                        }
21610                        if !matches!(self.peek(), Token::Join) {
21611                            return Err(self.err(format!(
21612                                "expected JOIN after FULL [OUTER], got {:?}",
21613                                self.peek()
21614                            )));
21615                        }
21616                        self.advance();
21617                        JoinKind::FullOuter
21618                    }
21619                    Token::Join => {
21620                        self.advance();
21621                        JoinKind::Inner
21622                    }
21623                    _ => break,
21624                };
21625            let table = self.parse_table_ref()?;
21626            // v7.37.7 C.1 — USING (col_list) sugar. Desugars to
21627            // `prev_table.col1 = table.col1 AND prev_table.col2 = table.col2 …`
21628            // where prev_table is the most-recent left-side table
21629            // (the previous join's table if any, else the FROM primary).
21630            // PG semantics around column merging are richer (USING'd
21631            // cols become deduplicated single output columns); for
21632            // sugar purposes the predicate-only form covers the
21633            // baseline corpus shape and chained `… JOIN x USING (k)
21634            // JOIN y USING (k)` calls.
21635            // v7.37.16 — NATURAL joins carry no ON/USING clause; the
21636            // common columns resolve at execution time.
21637            if natural {
21638                joins.push(FromJoin {
21639                    kind,
21640                    table,
21641                    on: None,
21642                    using_cols: None,
21643                    natural: true,
21644                });
21645                continue;
21646            }
21647            let using_match = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("using"));
21648            // v7.37.16 — capture the USING column list (in addition to
21649            // the ON desugar below) so the executor can perform PG's
21650            // column-merge on the output side.
21651            let mut using_cols: Option<Vec<String>> = None;
21652            let on = if matches!(self.peek(), Token::On) {
21653                self.advance();
21654                Some(self.parse_expr(0)?)
21655            } else if using_match {
21656                self.advance();
21657                if !matches!(self.peek(), Token::LParen) {
21658                    return Err(
21659                        self.err(format!("expected '(' after USING, got {:?}", self.peek()))
21660                    );
21661                }
21662                self.advance();
21663                let mut cols: Vec<String> = Vec::new();
21664                loop {
21665                    match self.peek().clone() {
21666                        Token::Ident(s) | Token::QuotedIdent(s) => {
21667                            self.advance();
21668                            cols.push(s);
21669                        }
21670                        other => {
21671                            return Err(self.err(format!(
21672                                "expected column name inside USING (…), got {other:?}"
21673                            )));
21674                        }
21675                    }
21676                    match self.peek() {
21677                        Token::Comma => {
21678                            self.advance();
21679                            continue;
21680                        }
21681                        Token::RParen => {
21682                            self.advance();
21683                            break;
21684                        }
21685                        other => {
21686                            return Err(self.err(format!(
21687                                "expected ',' or ')' inside USING (…), got {other:?}"
21688                            )));
21689                        }
21690                    }
21691                }
21692                if cols.is_empty() {
21693                    return Err(self.err("USING (…) requires at least one column".to_string()));
21694                }
21695                using_cols = Some(cols.clone());
21696                // Pick the left-side alias: prev join's table if any,
21697                // else FROM primary. Use alias when present, else
21698                // table name (PG-equivalent qualifier).
21699                let left_qual: String = joins
21700                    .last()
21701                    .map(|j| {
21702                        j.table
21703                            .alias
21704                            .clone()
21705                            .unwrap_or_else(|| j.table.name.clone())
21706                    })
21707                    .unwrap_or_else(|| alloc::string::String::from(left_primary_qual));
21708                let right_qual = table.alias.clone().unwrap_or_else(|| table.name.clone());
21709                let mut iter = cols.into_iter().map(|c| Expr::Binary {
21710                    lhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21711                        qualifier: Some(left_qual.clone()),
21712                        name: c.clone(),
21713                    })),
21714                    op: crate::ast::BinOp::Eq,
21715                    rhs: alloc::boxed::Box::new(Expr::Column(crate::ast::ColumnName {
21716                        qualifier: Some(right_qual.clone()),
21717                        name: c,
21718                    })),
21719                });
21720                let first = iter.next().expect("at least one col");
21721                Some(iter.fold(first, |acc, pred| Expr::Binary {
21722                    lhs: alloc::boxed::Box::new(acc),
21723                    op: crate::ast::BinOp::And,
21724                    rhs: alloc::boxed::Box::new(pred),
21725                }))
21726            } else if kind == JoinKind::Cross {
21727                None
21728            } else {
21729                return Err(self.err(format!(
21730                    "expected ON or USING after {:?} JOIN, got {:?}",
21731                    kind,
21732                    self.peek()
21733                )));
21734            };
21735            joins.push(FromJoin {
21736                kind,
21737                table,
21738                on,
21739                using_cols,
21740                natural: false,
21741            });
21742        }
21743        Ok(joins)
21744    }
21745
21746    /// Optional alias after an expression or table:
21747    /// `AS <ident>` is unambiguous; a bare `<ident>` directly after is also
21748    /// accepted (PG-style implicit alias). Returns `None` if the next token
21749    /// is not alias-shaped (e.g. comma, FROM, WHERE, semicolon, EOF, operator).
21750    fn parse_optional_alias(&mut self) -> Result<Option<String>, ParseError> {
21751        if matches!(self.peek(), Token::As) {
21752            self.advance();
21753            // v7.39 (round 340, V56) — after AS the next token MUST be an
21754            // identifier. This used to return None and "let the caller
21755            // surface the error on the next expectation", but when AS is
21756            // the LAST token there is no next expectation: `SELECT 1 AS`
21757            // parsed clean and silently dropped the alias. PG rejects it.
21758            // v7.40.11 — a keyword is a legal alias after AS.
21759            //
21760            // `expect_ident_like` has known the unreserved class since
21761            // v7.17, and this guard never let a keyword token reach it.
21762            // So every one of them was a syntax error in alias position
21763            // while being accepted as a column name in the same build:
21764            //
21765            //   CREATE TABLE rk (release int)   accepted
21766            //   SELECT release FROM rk          accepted
21767            //   SELECT 1 AS release             syntax error
21768            //
21769            // Reported against 7.40.9 for `release` and `savepoint` —
21770            // two statements in a shipped subcommand of the reporter's
21771            // could not be parsed — and it is the whole class, `show`
21772            // and `index` included.
21773            //
21774            // Measured on PG 18.6: after AS, EVERY keyword is a legal
21775            // label, `limit` and `between` included. This accepts the
21776            // ones this parser can name, which is the unreserved class;
21777            // a reserved keyword after AS is still refused here and PG
21778            // takes it.
21779            if let Token::Ident(_) | Token::QuotedIdent(_) = self.peek() {
21780                return self.expect_ident_like().map(Some);
21781            }
21782            if unreserved_keyword_text(self.peek()).is_some() {
21783                return self.expect_ident_like().map(Some);
21784            }
21785            return Err(self.err(alloc::format!(
21786                "expected an alias after AS, got {:?}",
21787                self.peek()
21788            )));
21789        }
21790        // v7.17.0 Phase 1.3 — implicit alias (no `AS`). PG's
21791        // grammar reserves a long list of follow-keywords from the
21792        // alias slot. SPG's bareword approximation: skip a small
21793        // set of idents that would otherwise be swallowed as the
21794        // table alias and break trailing clauses like CREATE
21795        // MATERIALIZED VIEW … WITH [NO] DATA or future ON
21796        // CONFLICT WHERE shapes.
21797        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
21798            if is_alias_stopword(s) {
21799                return Ok(None);
21800            }
21801            return Ok(self.expect_ident_like().ok());
21802        }
21803        // v7.40.11 — and a keyword, WITHOUT `AS`, which PG also takes:
21804        // `SELECT 1 release` answers 1 there.
21805        //
21806        // Not the ones that begin a trailing clause. PG reserves those
21807        // for exactly this reason and so must this: measured on PG 18.6,
21808        // `SELECT 1 limit` is `syntax error at end of input` — it read
21809        // `limit` as the clause, not as a label. Swallowing it here
21810        // would turn `SELECT 1 limit 2` into a two-token nonsense.
21811        if !matches!(self.peek(), Token::Limit | Token::Offset)
21812            && unreserved_keyword_text(self.peek()).is_some()
21813        {
21814            return Ok(self.expect_ident_like().ok());
21815        }
21816        Ok(None)
21817    }
21818
21819    /// Pratt loop. `min_prec` is the minimum binary-op precedence we'll accept.
21820    fn parse_expr(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
21821        // v7.30.2 (mailrs round-25 ask 2) — nesting budget: a parse
21822        // error beats a stack overflow (an overflow aborts the
21823        // embedding host process).
21824        self.enter_nested()?;
21825        let r = self.parse_expr_inner(min_prec);
21826        self.nest_depth -= 1;
21827        r
21828    }
21829
21830    /// `OPERATOR([schema.]<op>)` — PG's explicit-operator spelling.
21831    /// When the upcoming tokens form one, return the underlying
21832    /// operator token and the position just past the closing paren
21833    /// so the binary loop can dispatch on the plain operator.
21834    fn peek_explicit_operator(&self) -> Option<(usize, Token)> {
21835        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("operator")) {
21836            return None;
21837        }
21838        if !matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) {
21839            return None;
21840        }
21841        let mut i = self.pos + 2;
21842        // Optional schema qualifier (pg_catalog.<op> etc.).
21843        if matches!(self.tokens.get(i), Some(Token::Ident(_)))
21844            && matches!(self.tokens.get(i + 1), Some(Token::Dot))
21845        {
21846            i += 2;
21847        }
21848        let op_tok = self.tokens.get(i)?.clone();
21849        if !matches!(self.tokens.get(i + 1), Some(Token::RParen)) {
21850            return None;
21851        }
21852        Some((i + 2, op_tok))
21853    }
21854
21855    /// PG operator symbols that lower onto function calls in
21856    /// binary position: `~` / `~*` / `!~` / `!~*` (regex match
21857    /// family → regexp_like, comparison rung), `^@` (starts_with,
21858    /// comparison rung), `^` (power, tighter than `*`), `#`
21859    /// (integer XOR via `(a|b) - (a&b)` — the AND bits are a
21860    /// subset of the OR bits so the subtraction never borrows).
21861    fn try_symbol_operator(
21862        &mut self,
21863        lhs: &Expr,
21864        min_prec: u8,
21865    ) -> Result<Option<Expr>, ParseError> {
21866        enum Sym {
21867            Regex { ci: bool, negated: bool },
21868            Like { ci: bool, negated: bool },
21869            StartsWith,
21870            Power,
21871            Xor,
21872            RangeAdjacent,
21873        }
21874        // v7.39 (IS-precedence knife) — the low-precedence postfix
21875        // predicates ride this existing leaf call (zero new frame slots
21876        // on the nesting chain).
21877        if let Some(e) = self.parse_postfix_predicate(lhs, min_prec)? {
21878            return Ok(Some(e));
21879        }
21880        let (sym, prec): (Sym, u8) = match self.peek() {
21881            Token::Tilde => (
21882                Sym::Regex {
21883                    ci: false,
21884                    negated: false,
21885                },
21886                5,
21887            ),
21888            Token::TildeStar => (
21889                Sym::Regex {
21890                    ci: true,
21891                    negated: false,
21892                },
21893                5,
21894            ),
21895            Token::NotTilde => (
21896                Sym::Regex {
21897                    ci: false,
21898                    negated: true,
21899                },
21900                5,
21901            ),
21902            Token::NotTildeStar => (
21903                Sym::Regex {
21904                    ci: true,
21905                    negated: true,
21906                },
21907                5,
21908            ),
21909            // v7.37 D.25 — PG operator spellings of LIKE/ILIKE.
21910            Token::DoubleTilde => (
21911                Sym::Like {
21912                    ci: false,
21913                    negated: false,
21914                },
21915                5,
21916            ),
21917            Token::DoubleTildeStar => (
21918                Sym::Like {
21919                    ci: true,
21920                    negated: false,
21921                },
21922                5,
21923            ),
21924            Token::NotDoubleTilde => (
21925                Sym::Like {
21926                    ci: false,
21927                    negated: true,
21928                },
21929                5,
21930            ),
21931            Token::NotDoubleTildeStar => (
21932                Sym::Like {
21933                    ci: true,
21934                    negated: true,
21935                },
21936                5,
21937            ),
21938            Token::CaretAt => (Sym::StartsWith, 5),
21939            // PG `^` is exponentiation; MySQL `^` is bitwise XOR (and binds
21940            // tighter than `* / & |`, which the prec-9 rung preserves —
21941            // v7.39 round 407: +1 from the pre-XOR ladder's rung 8).
21942            Token::Caret if self.mysql_dialect => (Sym::Xor, 9),
21943            Token::Caret => (Sym::Power, 9),
21944            // v7.39 (round 760, F31-B1) — `#` is a generic operator too:
21945            // PG answers `5 # 3 + 1` as `5 # 4` = 1 (additive first).
21946            Token::Hash => (Sym::Xor, 6),
21947            Token::Adjacent => (Sym::RangeAdjacent, 5),
21948            _ => return Ok(None),
21949        };
21950        if prec < min_prec {
21951            return Ok(None);
21952        }
21953        self.advance();
21954        let rhs = self.parse_expr(prec + 1)?;
21955        let out = match sym {
21956            Sym::Regex { ci, negated } => {
21957                let mut args = alloc::vec![lhs.clone(), rhs];
21958                if ci {
21959                    args.push(Expr::Literal(Literal::String(String::from("i"))));
21960                }
21961                maybe_not(
21962                    Expr::FunctionCall {
21963                        name: String::from("regexp_like"),
21964                        args,
21965                    },
21966                    negated,
21967                )
21968            }
21969            Sym::Like { ci, negated } => Expr::Like {
21970                expr: alloc::boxed::Box::new(lhs.clone()),
21971                pattern: alloc::boxed::Box::new(rhs),
21972                negated,
21973                case_insensitive: ci,
21974            },
21975            Sym::StartsWith => Expr::FunctionCall {
21976                name: String::from("starts_with"),
21977                args: alloc::vec![lhs.clone(), rhs],
21978            },
21979            Sym::Power => Expr::FunctionCall {
21980                name: String::from("power"),
21981                args: alloc::vec![lhs.clone(), rhs],
21982            },
21983            // `#` bitwise XOR — a real operator now (was desugared to
21984            // `(a|b)-(a&b)`, algebraically identical for integers but
21985            // undefined for bit strings; the direct op handles both).
21986            Sym::Xor => Expr::Binary {
21987                lhs: Box::new(lhs.clone()),
21988                op: BinOp::BitXor,
21989                rhs: Box::new(rhs),
21990            },
21991            // range `-|-` "is adjacent to" — lowered to a catalog function.
21992            Sym::RangeAdjacent => Expr::FunctionCall {
21993                name: String::from("range_adjacent"),
21994                args: alloc::vec![lhs.clone(), rhs],
21995            },
21996        };
21997        Ok(Some(out))
21998    }
21999
22000    /// v7.39 (IS-precedence knife) — the LOW-precedence postfix
22001    /// predicates, moved out of the tight postfix-cast loop: PG binds
22002    /// `IS [NOT] NULL/TRUE/FALSE/UNKNOWN/DISTINCT FROM/JSON/NORMALIZED`
22003    /// looser than EVERY binary operator (only NOT/AND/OR are looser),
22004    /// and BETWEEN/IN/LIKE/ILIKE/SIMILAR at the comparison rung — so
22005    /// `1 + 1 IS NULL` is `(1+1) IS NULL`, not `1 + (1 IS NULL)`.
22006    /// Returns Ok(consumed expr) when a predicate fired, Err(expr back)
22007    /// when nothing at this position belongs to the family. Out-of-line
22008    /// (`inline(never)`): the caller sits on the per-nesting-level frame
22009    /// chain that MAX_NEST_DEPTH is tuned against.
22010    #[inline(never)]
22011    fn parse_postfix_predicate(
22012        &mut self,
22013        lhs: &Expr,
22014        min_prec: u8,
22015    ) -> Result<Option<Expr>, ParseError> {
22016        // Reached through try_symbol_operator (an existing leaf call of
22017        // the binary loop) so NO new stack slots land on the per-nesting
22018        // frame chain; the lhs clones only when a predicate actually
22019        // consumes it.
22020        match self.peek() {
22021            // v7.39 (round 407) — IS is rung 4, the BETWEEN/IN/LIKE
22022            // comparison family rung 5 (each +1 from the pre-XOR ladder).
22023            Token::Is if min_prec <= 4 => {}
22024            Token::Between | Token::In | Token::Like if min_prec <= 5 => {}
22025            Token::Not
22026                if min_prec <= 5
22027                    && matches!(
22028                        self.tokens.get(self.pos + 1),
22029                        Some(Token::Between | Token::In | Token::Like)
22030                    ) => {}
22031            Token::Not | Token::Ident(_)
22032                if min_prec <= 5
22033                    && (matches!(self.peek(), Token::Ident(s)
22034                            if s.eq_ignore_ascii_case("ilike")
22035                                || (self.mysql_dialect
22036                                    && (s.eq_ignore_ascii_case("regexp")
22037                                        || s.eq_ignore_ascii_case("rlike")))
22038                                || (s.eq_ignore_ascii_case("similar")
22039                                    && matches!(self.tokens.get(self.pos + 1), Some(Token::To))))
22040                        || (matches!(self.peek(), Token::Not)
22041                            && matches!(self.tokens.get(self.pos + 1),
22042                                Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22043                                    || (self.mysql_dialect
22044                                        && (s.eq_ignore_ascii_case("regexp")
22045                                            || s.eq_ignore_ascii_case("rlike")))
22046                                    || s.eq_ignore_ascii_case("similar")))) => {}
22047            _ => return Ok(None),
22048        }
22049        let mut expr = lhs.clone();
22050        // IS family: rung 4 (NOT's operand parses at 4, so `NOT x IS NULL`
22051        // still groups as NOT (x IS NULL); OR/XOR/AND at 1-3 stay outside).
22052        if min_prec <= 4 {
22053            if matches!(self.peek(), Token::Is) {
22054                self.advance();
22055                let negated = if matches!(self.peek(), Token::Not) {
22056                    self.advance();
22057                    true
22058                } else {
22059                    false
22060                };
22061                // v7.9.27b — `IS [NOT] DISTINCT FROM <rhs>`.
22062                // mailrs pg_dump.
22063                if matches!(self.peek(), Token::Distinct) {
22064                    self.advance();
22065                    if !matches!(self.peek(), Token::From) {
22066                        return Err(self.err(format!(
22067                            "expected FROM after IS{} DISTINCT, got {:?}",
22068                            if negated { " NOT" } else { "" },
22069                            self.peek()
22070                        )));
22071                    }
22072                    self.advance();
22073                    // Right-hand side: parse at the same precedence
22074                    // tier as comparison (rung 5) so `x IS DISTINCT FROM a + b`
22075                    // groups as `x IS DISTINCT FROM (a + b)`.
22076                    let rhs = self.parse_expr(5)?;
22077                    let op = if negated {
22078                        BinOp::IsNotDistinctFrom
22079                    } else {
22080                        BinOp::IsDistinctFrom
22081                    };
22082                    expr = Expr::Binary {
22083                        op,
22084                        lhs: Box::new(expr),
22085                        rhs: Box::new(rhs),
22086                    };
22087                    {
22088                        return Ok(Some(expr));
22089                    }
22090                }
22091                // v7.37.17 (17.6 siblings) — SQL:2016 / PG 16
22092                // `IS [NOT] JSON [VALUE|OBJECT|ARRAY|SCALAR]`.
22093                // Lowers onto pg_is_json(x, kind); NOT wraps the
22094                // call in a logical negation.
22095                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22096                if s.eq_ignore_ascii_case("json"))
22097                {
22098                    self.advance(); // JSON
22099                    let kind = match self.peek() {
22100                        Token::Ident(s) | Token::QuotedIdent(s)
22101                            if matches!(
22102                                s.to_ascii_lowercase().as_str(),
22103                                "value" | "object" | "array" | "scalar"
22104                            ) =>
22105                        {
22106                            let k = s.to_ascii_lowercase();
22107                            self.advance();
22108                            k
22109                        }
22110                        _ => "value".to_string(),
22111                    };
22112                    let call = Expr::FunctionCall {
22113                        name: "pg_is_json".to_string(),
22114                        args: alloc::vec![expr, Expr::Literal(Literal::String(kind)),],
22115                    };
22116                    expr = if negated {
22117                        Expr::Unary {
22118                            op: UnOp::Not,
22119                            expr: Box::new(call),
22120                        }
22121                    } else {
22122                        call
22123                    };
22124                    {
22125                        return Ok(Some(expr));
22126                    }
22127                }
22128                // v7.38 (read01 sweep) — SQL:2016 `x IS [NOT] [form]
22129                // NORMALIZED` (form ∈ NFC/NFD/NFKC/NFKD, default NFC).
22130                // Lowers onto is_normalized(x [, 'FORM']); NOT negates.
22131                {
22132                    let form_kw = match self.peek() {
22133                        Token::Ident(s) | Token::QuotedIdent(s)
22134                            if matches!(
22135                                s.to_ascii_uppercase().as_str(),
22136                                "NFC" | "NFD" | "NFKC" | "NFKD"
22137                            ) && matches!(
22138                                self.tokens.get(self.pos + 1),
22139                                Some(Token::Ident(n) | Token::QuotedIdent(n))
22140                                    if n.eq_ignore_ascii_case("normalized")
22141                            ) =>
22142                        {
22143                            Some(s.to_ascii_uppercase())
22144                        }
22145                        _ => None,
22146                    };
22147                    let bare_normalized = form_kw.is_none()
22148                        && matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22149                        if s.eq_ignore_ascii_case("normalized"));
22150                    if form_kw.is_some() || bare_normalized {
22151                        if form_kw.is_some() {
22152                            self.advance(); // form keyword
22153                        }
22154                        self.advance(); // NORMALIZED
22155                        let mut args = alloc::vec![expr];
22156                        if let Some(f) = form_kw {
22157                            args.push(Expr::Literal(Literal::String(f)));
22158                        }
22159                        let call = Expr::FunctionCall {
22160                            name: "is_normalized".to_string(),
22161                            args,
22162                        };
22163                        expr = if negated {
22164                            Expr::Unary {
22165                                op: UnOp::Not,
22166                                expr: Box::new(call),
22167                            }
22168                        } else {
22169                            call
22170                        };
22171                        {
22172                            return Ok(Some(expr));
22173                        }
22174                    }
22175                }
22176                // `x IS [NOT] TRUE | FALSE | UNKNOWN` — the
22177                // three-valued boolean tests. IS TRUE/FALSE never
22178                // return NULL, so they lower to CASE forms whose
22179                // ELSE catches the NULL branch; IS UNKNOWN on a
22180                // boolean is exactly IS NULL.
22181                if matches!(self.peek(), Token::True | Token::False)
22182                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("unknown"))
22183                {
22184                    let tok = self.advance();
22185                    let test = match tok {
22186                        Token::True => Some(true),
22187                        Token::False => Some(false),
22188                        _ => None, // UNKNOWN
22189                    };
22190                    // v7.39 (round 328, V45) — kept as what the user
22191                    // wrote. These used to be lowered here into `CASE` /
22192                    // `IS NULL`; the semantics were right but the AST no
22193                    // longer knew the form, so `CHECK ((a > 1) IS TRUE)`
22194                    // was echoed back as
22195                    // `CHECK ((CASE WHEN (a > 1) THEN TRUE ELSE FALSE END))`.
22196                    expr = Expr::BoolTest {
22197                        expr: Box::new(expr),
22198                        value: test,
22199                        negated,
22200                    };
22201                    {
22202                        return Ok(Some(expr));
22203                    }
22204                }
22205                if !matches!(self.peek(), Token::Null) {
22206                    return Err(self.err(format!(
22207                    "expected NULL, DISTINCT, JSON, TRUE, FALSE or UNKNOWN after IS{}, got {:?}",
22208                    if negated { " NOT" } else { "" },
22209                    self.peek()
22210                )));
22211                }
22212                self.advance();
22213                expr = Expr::IsNull {
22214                    expr: Box::new(expr),
22215                    negated,
22216                };
22217                {
22218                    return Ok(Some(expr));
22219                }
22220            }
22221        }
22222        // BETWEEN / IN / LIKE / ILIKE / SIMILAR: comparison rung (5).
22223        if min_prec <= 5 {
22224            // `x [NOT] BETWEEN a AND b`, `x [NOT] IN (...)`, `x [NOT] LIKE p`.
22225            // Look one token ahead so a stray `NOT` not followed by any of
22226            // these flows through to the early return below untouched.
22227            let negated = if matches!(self.peek(), Token::Not) {
22228                let next = self.tokens.get(self.pos + 1);
22229                matches!(next, Some(Token::Between | Token::In | Token::Like))
22230                    || matches!(next, Some(Token::Ident(s)) if s.eq_ignore_ascii_case("ilike")
22231                    || (self.mysql_dialect
22232                        && (s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike")))
22233                    || s.eq_ignore_ascii_case("similar"))
22234            } else {
22235                false
22236            };
22237            if negated {
22238                self.advance();
22239            }
22240            if matches!(self.peek(), Token::Between) {
22241                expr = self.parse_between_tail(expr, negated)?;
22242                {
22243                    return Ok(Some(expr));
22244                }
22245            }
22246            if matches!(self.peek(), Token::In) {
22247                if self.suppress_in_tail && !negated {
22248                    // POSITION(sub IN str) — IN belongs to the
22249                    // enclosing function syntax; stop here.
22250                    {
22251                        return Ok(None);
22252                    }
22253                }
22254                expr = self.parse_in_tail(expr, negated)?;
22255                {
22256                    return Ok(Some(expr));
22257                }
22258            }
22259            // v7.39 (read01 regexp.c) — `x [NOT] SIMILAR TO p [ESCAPE e]`
22260            // lowers onto the internal __similar_to(expr, pat[, esc]) call
22261            // (the SQL→regex transform runs inside, in the backtracking-
22262            // friendly shape SPG's matcher needs).
22263            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
22264                && matches!(self.tokens.get(self.pos + 1), Some(Token::To))
22265            {
22266                self.advance(); // SIMILAR
22267                self.advance(); // TO
22268                let pattern = self.parse_expr(6)?;
22269                let mut args = alloc::vec![expr, pattern];
22270                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22271                    self.advance();
22272                    args.push(self.parse_expr(6)?);
22273                }
22274                let call = Expr::FunctionCall {
22275                    name: "__similar_to".to_string(),
22276                    args,
22277                };
22278                expr = maybe_not(call, negated);
22279                {
22280                    return Ok(Some(expr));
22281                }
22282            }
22283            if matches!(self.peek(), Token::Like) {
22284                self.advance();
22285                // `x [NOT] LIKE ANY/ALL (ARRAY[...])` — quantified LIKE.
22286                if let Some(q) = self.try_like_any_all(&expr, negated, false)? {
22287                    expr = q;
22288                    {
22289                        return Ok(Some(expr));
22290                    }
22291                }
22292                // Pattern at the same precedence as other comparison RHSes —
22293                // 5 leaves AND/OR alone so `a LIKE 'x%' AND b` parses right.
22294                let mut pattern = self.parse_expr(6)?;
22295                // `ESCAPE 'c'` — rewrite a literal pattern to the
22296                // default backslash escape at parse time. Custom
22297                // escapes on non-literal patterns would need
22298                // matcher support; error honestly.
22299                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape")) {
22300                    self.advance();
22301                    let esc = self.parse_expr(6)?;
22302                    pattern = Self::rewrite_like_escape(pattern, esc).map_err(|m| self.err(m))?;
22303                }
22304                expr = Expr::Like {
22305                    expr: Box::new(expr),
22306                    pattern: Box::new(pattern),
22307                    negated,
22308                    case_insensitive: false,
22309                };
22310                {
22311                    return Ok(Some(expr));
22312                }
22313            }
22314            // v7.25 (round-17) — ILIKE: case-insensitive LIKE. The
22315            // keyword reaches us as a plain identifier.
22316            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("ilike")) {
22317                self.advance();
22318                if let Some(q) = self.try_like_any_all(&expr, negated, true)? {
22319                    expr = q;
22320                    {
22321                        return Ok(Some(expr));
22322                    }
22323                }
22324                let pattern = self.parse_expr(6)?;
22325                expr = Expr::Like {
22326                    expr: Box::new(expr),
22327                    pattern: Box::new(pattern),
22328                    negated,
22329                    case_insensitive: true,
22330                };
22331                {
22332                    return Ok(Some(expr));
22333                }
22334            }
22335            // v7.39 (round 380) — MySQL's REGEXP / RLIKE regex-match
22336            // operator (RLIKE is the alias). It is a keyword, not `~`, and
22337            // matches case-insensitively under the default collation, so it
22338            // lowers onto the same `regexp_like(expr, pattern, 'i')` the
22339            // `~*` operator uses, wrapped in NOT when negated.
22340            if self.mysql_dialect
22341                && matches!(self.peek(), Token::Ident(s)
22342                    if s.eq_ignore_ascii_case("regexp") || s.eq_ignore_ascii_case("rlike"))
22343            {
22344                self.advance();
22345                let pattern = self.parse_expr(6)?;
22346                let call = Expr::FunctionCall {
22347                    name: String::from("regexp_like"),
22348                    args: alloc::vec![
22349                        expr,
22350                        pattern,
22351                        Expr::Literal(Literal::String(String::from("i"))),
22352                    ],
22353                };
22354                return Ok(Some(maybe_not(call, negated)));
22355            }
22356        }
22357        let _ = expr;
22358        Ok(None)
22359    }
22360
22361    fn parse_expr_inner(&mut self, min_prec: u8) -> Result<Expr, ParseError> {
22362        let mut lhs = self.parse_unary()?;
22363        let mut chain_len = 0usize;
22364        loop {
22365            // OPERATOR([schema.]op) reduces to its underlying
22366            // operator token before the normal dispatch.
22367            let explicit = self.peek_explicit_operator();
22368            let dispatch = match &explicit {
22369                Some((_, tok)) => self.binop_here(tok),
22370                None => self.binop_here(self.peek()),
22371            };
22372            let Some((op, prec)) = dispatch else {
22373                // v7.39 (round 539) — `OPERATOR(pg_catalog.~)` and the rest
22374                // of the symbol family. `binop_here` answers None for them
22375                // because they lower onto function calls rather than a
22376                // BinOp, and the fallback below reads `self.peek()` — the
22377                // word OPERATOR, not the operator. `pg_dump` writes every
22378                // catalog predicate this way, so its first query failed
22379                // and no dump ran:
22380                //
22381                //   AND c.relname OPERATOR(pg_catalog.~) '^(t)$'
22382                //
22383                // Collapsing the wrapper to the operator it names puts the
22384                // token where the fallback already looks.
22385                if let Some((next, op_tok)) = explicit {
22386                    self.tokens.splice(self.pos..next, [op_tok]);
22387                }
22388                if let Some(e) = self.try_symbol_operator(&lhs, min_prec)? {
22389                    lhs = e;
22390                    chain_len += 1;
22391                    if chain_len > MAX_BINARY_CHAIN {
22392                        return Err(self.err(alloc::format!(
22393                            "more than {MAX_BINARY_CHAIN} chained binary operators"
22394                        )));
22395                    }
22396                    continue;
22397                }
22398                break;
22399            };
22400            if prec < min_prec {
22401                break;
22402            }
22403            // v7.30.2 (mailrs round-25 ask 2) — the chain builds
22404            // iteratively but evaluates and drops recursively;
22405            // depth beyond the budget overflows worker stacks.
22406            chain_len += 1;
22407            if chain_len > MAX_BINARY_CHAIN {
22408                return Err(self.err(alloc::format!(
22409                    "more than {MAX_BINARY_CHAIN} chained binary operators; rewrite long OR-equality chains as IN (…)"
22410                )));
22411            }
22412            match explicit {
22413                Some((end_pos, _)) => self.pos = end_pos,
22414                None => {
22415                    self.advance();
22416                }
22417            }
22418            // v7.10.12 — `x <op> ANY(arr)` / `x <op> ALL(arr)`.
22419            // ANY is a bare ident; ALL is a reserved Token. Both
22420            // require an immediate `(` to disambiguate from
22421            // identifier columns named `any` / `all`.
22422            let any_kind = match self.peek() {
22423                Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => {
22424                    Some(false)
22425                }
22426                Token::Ident(s) | Token::QuotedIdent(s)
22427                    if (s.eq_ignore_ascii_case("any")
22428                        || s.eq_ignore_ascii_case("some")
22429                        || s.eq_ignore_ascii_case("all"))
22430                        && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
22431                {
22432                    Some(!s.eq_ignore_ascii_case("all"))
22433                }
22434                _ => None,
22435            };
22436            if let Some(is_any) = any_kind {
22437                lhs = self.parse_any_all_rhs(lhs, op, is_any)?;
22438                continue;
22439            }
22440            let rhs = self.parse_expr(prec + 1)?;
22441            lhs = Expr::Binary {
22442                lhs: Box::new(lhs),
22443                op,
22444                rhs: Box::new(rhs),
22445            };
22446        }
22447        Ok(lhs)
22448    }
22449
22450    /// `x <op> ANY (…)` / `ALL (…)`, both the quantified-subquery form
22451    /// and the array form.
22452    ///
22453    /// `#[inline(never)]` and out of `parse_expr_inner`, which sits on the
22454    /// frame chain `MAX_NEST_DEPTH` is tuned against: a debug build gives
22455    /// this block's `Expr` temporaries and four `format!` sites slots in
22456    /// that frame on every level of `((((1))))`, which never reaches it.
22457    #[inline(never)]
22458    fn parse_any_all_rhs(
22459        &mut self,
22460        lhs: Expr,
22461        op: BinOp,
22462        is_any: bool,
22463    ) -> Result<Expr, ParseError> {
22464        self.advance(); // ident
22465        self.advance(); // (
22466        // `x op ANY (SELECT …)` — the quantified-subquery
22467        // form. `= ANY` is exactly IN; the other operators
22468        // lower onto EXISTS over the subquery as a derived
22469        // table, comparing against its single projection
22470        // aliased __v (x's columns resolve correlated).
22471        // ALL is the negated-EXISTS complement; a NULL
22472        // element makes PG return NULL where this lowering
22473        // returns true — the NOT NULL column case (the
22474        // practical one) is exact.
22475        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
22476            // v7.39 (round 153) — `ANY (WITH … SELECT …)` is
22477            // legal PG too (round-151 sibling). Out-of-line
22478            // (#[inline(never)] helper) — this sits on
22479            // parse_expr's recursive frame and the two-armed
22480            // SELECT temporary blew the nesting-budget stack.
22481            let mut sub = self.parse_any_all_select_body()?;
22482            if !matches!(self.peek(), Token::RParen) {
22483                return Err(self.err(alloc::format!(
22484                    "expected ')' after ANY/ALL subquery, got {:?}",
22485                    self.peek()
22486                )));
22487            }
22488            self.advance();
22489            if sub.items.len() != 1 {
22490                return Err(self.err(alloc::format!(
22491                    "ANY/ALL subquery must return one column, got {}",
22492                    sub.items.len()
22493                )));
22494            }
22495            if is_any && matches!(op, BinOp::Eq) {
22496                return Ok(Expr::InSubquery {
22497                    expr: Box::new(lhs),
22498                    subquery: Box::new(sub),
22499                    negated: false,
22500                });
22501            }
22502            // The engine's subquery resolvers materialise
22503            // the single-column result into an ARRAY the
22504            // existing AnyAll three-valued eval consumes.
22505            return Ok(Expr::AnyAll {
22506                expr: Box::new(lhs),
22507                op,
22508                array: Box::new(Expr::ScalarSubquery(Box::new(sub))),
22509                is_any,
22510            });
22511        }
22512        let arr = self.parse_expr(0)?;
22513        if !matches!(self.peek(), Token::RParen) {
22514            return Err(self.err(alloc::format!(
22515                "expected ')' after ANY/ALL argument, got {:?}",
22516                self.peek()
22517            )));
22518        }
22519        self.advance();
22520        Ok(Expr::AnyAll {
22521            expr: Box::new(lhs),
22522            op,
22523            array: Box::new(arr),
22524            is_any,
22525        })
22526    }
22527
22528    /// v7.39 (read01 geo_ops.c) — prefix `@@` (center-of). Out-of-line
22529    /// from `parse_unary` (see the frame-budget note at MAX_NEST_DEPTH).
22530    #[inline(never)]
22531    fn parse_prefix_center(&mut self) -> Result<Expr, ParseError> {
22532        self.advance();
22533        let e = self.parse_expr(9)?;
22534        Ok(build_center_call(e))
22535    }
22536
22537    /// v7.39 (round 508) — a prefix operator that IS a function: `@ x` is
22538    /// `abs(x)`, `# p` is `npoints(p)`, `@-@ p` is `length(p)`. Binds like
22539    /// unary minus.
22540    ///
22541    /// `#[inline(never)]` for the same reason as its neighbours: parse_unary
22542    /// sits on the recursive frame chain MAX_NEST_DEPTH is tuned against, so
22543    /// the Expr-sized local stays out of that frame.
22544    #[inline(never)]
22545    fn parse_prefix_call(&mut self, name: &str) -> Result<Expr, ParseError> {
22546        self.advance();
22547        let e = self.parse_expr(9)?;
22548        Ok(Expr::FunctionCall {
22549            name: alloc::string::String::from(name),
22550            args: alloc::vec![e],
22551        })
22552    }
22553
22554    /// v7.39 (read01 geo_ops.c) — prefix `?|` (vertical) / `?-`
22555    /// (horizontal). Out-of-line from `parse_unary` (frame budget).
22556    #[inline(never)]
22557    fn parse_prefix_geom_axis(&mut self, vertical: bool) -> Result<Expr, ParseError> {
22558        self.advance();
22559        let e = self.parse_expr(9)?;
22560        Ok(Expr::FunctionCall {
22561            name: alloc::string::String::from(if vertical {
22562                "isvertical"
22563            } else {
22564                "ishorizontal"
22565            }),
22566            args: alloc::vec![e],
22567        })
22568    }
22569
22570    /// v7.39 (round 355, M13) — `BINARY <expr>`, lowered onto the same
22571    /// cast the `CAST(x AS BINARY)` spelling produces. It binds tightly:
22572    /// MariaDB reads `BINARY 1 + 1` as `(BINARY 1) + 1` = 2.
22573    #[inline(never)]
22574    fn parse_binary_prefix(&mut self) -> Result<Expr, ParseError> {
22575        self.advance();
22576        let e = self.parse_expr(9)?;
22577        Ok(Expr::Cast {
22578            expr: Box::new(e),
22579            target: CastTarget::Named("binary".to_string()),
22580        })
22581    }
22582
22583    /// The prefix operators that share one shape: take the token, parse
22584    /// an operand at `prec`, wrap it.
22585    ///
22586    /// `#[inline(never)]`, and one function instead of five arms, for the
22587    /// reason the neighbouring `parse_prefix_*` helpers give: `parse_unary`
22588    /// sits on the frame chain `MAX_NEST_DEPTH` is tuned against, and a
22589    /// debug build gives EVERY arm's locals a slot in the frame, whichever
22590    /// arm runs. `((((1))))` reaches none of these arms and was carrying
22591    /// five `Expr`-sized locals per level for them anyway.
22592    #[inline(never)]
22593    fn parse_unary_op(&mut self, op: UnOp, prec: u8) -> Result<Expr, ParseError> {
22594        self.advance();
22595        let e = self.parse_expr(prec)?;
22596        Ok(Expr::Unary {
22597            op,
22598            expr: Box::new(e),
22599        })
22600    }
22601
22602    /// Unary minus. Out-of-line for the frame reason on `parse_unary_op`,
22603    /// and separate from it because of the literal folding below and the
22604    /// `format!` temporaries that folding needs.
22605    #[inline(never)]
22606    fn parse_prefix_minus(&mut self) -> Result<Expr, ParseError> {
22607        self.advance();
22608        // v7.39 (round 549) — fold the sign into an integer literal that
22609        // only fits once it is negative.
22610        //
22611        // `9223372036854775808` is one past i64::MAX, so the lexer hands
22612        // it over as a NUMERIC and `-` on a numeric stays numeric. PG
22613        // folds the sign first, so `-9223372036854775808` is a bigint
22614        // there — and `-9223372036854775808 - 1` raises "bigint out of
22615        // range" where SPG quietly answered -9223372036854775809, a value
22616        // no bigint can hold. The arithmetic itself was already checked;
22617        // only the literal's type was wrong.
22618        if let Token::Numeric(lit) = self.peek()
22619            && let Ok(folded) = alloc::format!("-{lit}").parse::<i64>()
22620        {
22621            self.advance();
22622            return Ok(Expr::Literal(Literal::Integer(folded)));
22623        }
22624        // Unary minus binds tighter than `*`/`/` (now at prec 7 after
22625        // `<->` slotted into 5 and arithmetic shifted up).
22626        let e = self.parse_expr(9)?;
22627        Ok(Expr::Unary {
22628            op: UnOp::Neg,
22629            expr: Box::new(e),
22630        })
22631    }
22632
22633    /// tsquery `!!` prefix negation, lowered to the catalog function.
22634    /// Binds like unary minus. Out-of-line for the frame reason on
22635    /// `parse_unary_op`.
22636    #[inline(never)]
22637    fn parse_prefix_tsquery_not(&mut self) -> Result<Expr, ParseError> {
22638        self.advance();
22639        let e = self.parse_expr(9)?;
22640        Ok(Expr::FunctionCall {
22641            name: String::from("tsquery_not"),
22642            args: alloc::vec![e],
22643        })
22644    }
22645
22646    fn parse_unary(&mut self) -> Result<Expr, ParseError> {
22647        match self.peek() {
22648            // NOT binds tighter than AND / XOR / OR but looser than
22649            // comparisons — its operand takes everything ≥ the comparison
22650            // rung (4), leaving AND (3) / XOR (2) / OR (1) outside so
22651            // `NOT a AND b` groups as `(NOT a) AND b`. (v7.39 round 407:
22652            // was rung 3, behaviour-identical when 3 was unused; AND now
22653            // occupies 3, so this must be 4 to keep NOT tighter than AND.)
22654            Token::Not => self.parse_unary_op(UnOp::Not, 4),
22655            // v7.39 (round 355, M13) — MySQL's `BINARY <expr>` prefix.
22656            // The body is out-of-line: `parse_unary` is one of the three
22657            // frames the parser's MAX_NEST_DEPTH is tuned against, and an
22658            // inline arm here overflowed the native stack in
22659            // `nesting_budget_errors_cleanly` — the guard test caught it,
22660            // exactly as the eval-side cliff did in rounds 346 and 351.
22661            Token::Ident(w) if self.mysql_dialect && w.eq_ignore_ascii_case("binary") => {
22662                self.parse_binary_prefix()
22663            }
22664            // v7.39 (round 353, M10) — MySQL's `!`. It binds TIGHTER than
22665            // arithmetic, unlike NOT: MariaDB answers 1 for `!1 + 1`
22666            // (`(!1)+1`) and 0 for `NOT 1 + 1` (`NOT (1+1)`), measured.
22667            Token::Bang => self.parse_unary_op(UnOp::Not, 9),
22668            Token::Minus => self.parse_prefix_minus(),
22669            // v7.39 (round 507) — unary `+`, which SPG did not have. `+1`
22670            // worked only because the lexer reads it as one signed literal;
22671            // `+ 1`, `+a`, `+(1)` and `1 + +1` were syntax errors, and both
22672            // PG18 and MariaDB take all of them. Binds like unary minus.
22673            Token::Plus => self.parse_unary_op(UnOp::Plus, 9),
22674            // Bitwise NOT binds like unary minus.
22675            Token::Tilde => self.parse_unary_op(UnOp::BitNot, 9),
22676            // v7.39 (read01 geo_ops.c) — prefix `@@` is PG's geometric
22677            // "center of" operator; desugars to center(x). The whole arm
22678            // is out-of-line: parse_unary sits on the per-nesting-level
22679            // frame chain that MAX_NEST_DEPTH is tuned against, so no
22680            // Expr-sized local may live in this frame.
22681            Token::TsMatch => self.parse_prefix_center(),
22682            // v7.39 (round 508) — the prefix operators that are named
22683            // functions in disguise: `@ x` is abs, `# p` is npoints, `@-@ p`
22684            // is length. Out-of-line for the same nesting-frame reason as
22685            // parse_prefix_center — parse_unary sits on the recursive cycle
22686            // MAX_NEST_DEPTH is tuned against, so no Expr-sized local may
22687            // live in this frame.
22688            Token::At => self.parse_prefix_call("abs"),
22689            Token::Hash => self.parse_prefix_call("npoints"),
22690            Token::AtMinusAt => self.parse_prefix_call("length"),
22691            // v7.39 (read01 geo_ops.c) — prefix `?|` / `?-`: "is vertical" /
22692            // "is horizontal" (lseg / line); desugars to the existing
22693            // isvertical()/ishorizontal() functions. Out-of-line for the
22694            // same nesting-frame reason as parse_prefix_center.
22695            Token::JsonKeysAny => self.parse_prefix_geom_axis(true),
22696            Token::GeomHoriz => self.parse_prefix_geom_axis(false),
22697            Token::DoubleBang => self.parse_prefix_tsquery_not(),
22698            _ => self.parse_atom(),
22699        }
22700    }
22701
22702    /// Parse a parenthesised scalar subquery body after the caller has consumed
22703    /// `(` and confirmed the next token is SELECT (or WITH, when `is_with`).
22704    /// v7.37 D.43 — `#[inline(never)]` keeps the large `Statement` local and the
22705    /// SELECT/WITH parse machinery off `parse_atom`'s stack frame; parse_atom sits
22706    /// on the recursive `((…))` cycle whose depth budget is tuned to that frame.
22707    /// v7.39 (read01 round 105) — is the current position `( <subquery-start>`,
22708    /// i.e. an `ARRAY(<subquery>)` and not `ARRAY[...]`? A subquery starts with
22709    /// SELECT, VALUES, or WITH (WITH lexes as a bare ident).
22710    /// `#[inline(never)]`: keeps this guard's locals off parse_atom's frame,
22711    /// which sits on the recursive nesting-budget cycle (a few extra bytes there
22712    /// tips the deep-nesting test into a stack overflow).
22713    #[inline(never)]
22714    fn array_subquery_ahead(&self) -> bool {
22715        if !matches!(self.peek(), Token::LParen) {
22716            return false;
22717        }
22718        matches!(
22719            self.tokens.get(self.pos + 1),
22720            Some(Token::Select | Token::Values)
22721        ) || matches!(
22722            self.tokens.get(self.pos + 1),
22723            Some(Token::Ident(w) | Token::QuotedIdent(w)) if w.eq_ignore_ascii_case("with")
22724        )
22725    }
22726
22727    /// v7.10.10 — `ARRAY[expr, …]` literal body. The `array` ident is consumed
22728    /// and the current token is `[`. `#[inline(never)]` so its `Vec`/loop
22729    /// locals stay off parse_atom's recursive frame (round 105).
22730    #[inline(never)]
22731    fn parse_array_literal_body(&mut self) -> Result<Expr, ParseError> {
22732        self.advance(); // consume `[`
22733        let mut items: Vec<Expr> = Vec::new();
22734        if !matches!(self.peek(), Token::RBracket) {
22735            loop {
22736                // Inside `ARRAY[...]`, a nested `[...]` is a sub-array
22737                // (`ARRAY[[1,2],[3,4]]`), not a pgvector literal.
22738                if matches!(self.peek(), Token::LBracket) {
22739                    items.push(self.parse_array_bracket_body()?);
22740                } else {
22741                    items.push(self.parse_expr(0)?);
22742                }
22743                match self.peek() {
22744                    Token::Comma => {
22745                        self.advance();
22746                    }
22747                    Token::RBracket => break,
22748                    other => {
22749                        return Err(self.err(alloc::format!(
22750                            "expected ',' or ']' in ARRAY literal, got {other:?}"
22751                        )));
22752                    }
22753                }
22754            }
22755        }
22756        self.advance(); // consume `]`
22757        Ok(Expr::Array(items))
22758    }
22759
22760    /// v7.39 (read01 round 105) — parse `ARRAY(<subquery>)`. The `array` ident
22761    /// is already consumed; the current token is `(`. Desugars to a scalar
22762    /// subquery `SELECT array_agg(c) FROM (<subquery>) AS t(c)`, which collects
22763    /// the subquery's single-column rows in order — reusing the existing
22764    /// ScalarSubquery machinery rather than adding an AST node. `#[inline(never)]`
22765    /// keeps the large `Statement` local off parse_atom's recursive frame.
22766    #[inline(never)]
22767    fn parse_array_subquery(&mut self) -> Result<Expr, ParseError> {
22768        self.advance(); // consume `(`
22769        let is_with = matches!(self.peek(), Token::Ident(w) | Token::QuotedIdent(w)
22770            if w.eq_ignore_ascii_case("with"));
22771        let sub = if is_with {
22772            self.advance(); // WITH
22773            self.parse_with_cte_then_select()?
22774        } else {
22775            self.parse_select_stmt()?
22776        };
22777        if !matches!(self.peek(), Token::RParen) {
22778            return Err(self.err(alloc::format!(
22779                "expected ')' to close ARRAY(subquery), got {:?}",
22780                self.peek()
22781            )));
22782        }
22783        self.advance(); // consume `)`
22784        // Reuse the parser to build the array_agg wrapper from the subquery's
22785        // canonical text — avoids hand-constructing the derived-table AST.
22786        let wrapper = alloc::format!(
22787            "SELECT array_agg(\"__spg_arr_c\") FROM ({sub}) AS \"__spg_arr_t\"(\"__spg_arr_c\")"
22788        );
22789        let stmt = parse_statement(&wrapper)
22790            .map_err(|e| self.err(alloc::format!("ARRAY(subquery): {}", e.message)))?;
22791        let Statement::Select(sel) = stmt else {
22792            return Err(self.err("ARRAY(subquery) did not desugar to a SELECT".into()));
22793        };
22794        Ok(Expr::ScalarSubquery(alloc::boxed::Box::new(sel)))
22795    }
22796
22797    #[inline(never)]
22798    fn parse_paren_scalar_subquery(&mut self, is_with: bool) -> Result<Expr, ParseError> {
22799        let inner = if is_with {
22800            self.advance(); // WITH
22801            self.parse_with_cte_then_select()?
22802        } else {
22803            self.parse_select_stmt()?
22804        };
22805        match self.advance() {
22806            Token::RParen => {
22807                let Statement::Select(s) = inner else {
22808                    return Err(ParseError {
22809                        message: "scalar subquery body must be a SELECT".into(),
22810                        token_pos: self.consumed_pos(),
22811                    });
22812                };
22813                Ok(Expr::ScalarSubquery(Box::new(s)))
22814            }
22815            other => Err(ParseError {
22816                message: format!("expected ')' after scalar subquery, got {other:?}"),
22817                token_pos: self.consumed_pos(),
22818            }),
22819        }
22820    }
22821
22822    /// `B'1010'` / `X'1F'` bit-string (PG) or binary-string (MySQL)
22823    /// literals. The lexer splits them into an ident + string; recombine
22824    /// here. Out-of-line and returning `Option` so `parse_atom` — the
22825    /// recursive frame the 768 KiB stack budget is tuned against — pays no
22826    /// frame for the `body` / `bits` strings and their char loops (the
22827    /// round-367 frame cliff, M20).
22828    #[inline(never)]
22829    fn try_parse_bit_string_literal(&mut self) -> Option<Result<Expr, ParseError>> {
22830        let is_hex = match self.peek() {
22831            Token::Ident(p) if p.eq_ignore_ascii_case("x") => true,
22832            Token::Ident(p) if p.eq_ignore_ascii_case("b") => false,
22833            _ => return None,
22834        };
22835        if !matches!(self.tokens.get(self.pos + 1), Some(Token::String(_))) {
22836            return None;
22837        }
22838        // v7.39.3 — where the LITERAL starts, because the errors below
22839        // are about the literal and both engines point at it. `err`
22840        // reports the CURRENT token, which by then is the one after the
22841        // string: `SELECT x'123'` pointed at Eof, so the MySQL wire's
22842        // `near '…'` snippet — which runs from the reported position to
22843        // the end — came out empty where MySQL 9.7.2 says `near
22844        // 'x'123''`.
22845        let lit_pos = self.pos;
22846        self.advance();
22847        let Token::String(body) = self.advance() else {
22848            unreachable!("guarded above");
22849        };
22850        // v7.39 (round 367, M20) — in the MySQL dialect `X'…'` and `b'…'`
22851        // are BINARY STRINGS, not PG bit strings. `X'41'` is the byte 0x41
22852        // (hex pairs, even count required — MariaDB errors on an odd
22853        // count); `b'1010'` packs its bits big-endian, left-padded to a
22854        // byte. Lower both onto the bytea cast.
22855        if self.mysql_dialect {
22856            if is_hex {
22857                if body.len() % 2 == 1 {
22858                    return Some(Err(self.err_at(
22859                        lit_pos,
22860                        alloc::format!("invalid hex string literal X'{body}': odd digit count"),
22861                    )));
22862                }
22863                for c in body.chars() {
22864                    if !c.is_ascii_hexdigit() {
22865                        return Some(Err(self.err_at(
22866                            lit_pos,
22867                            alloc::format!("invalid hexadecimal digit {c:?} in X'…'"),
22868                        )));
22869                    }
22870                }
22871                return Some(self.finish_postfix_casts(hex_literal_to_bytea_expr(&body)));
22872            }
22873            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22874                return Some(Err(self.err_at(
22875                    lit_pos,
22876                    alloc::format!("invalid binary digit {bad:?} in b'…'"),
22877                )));
22878            }
22879            return Some(self.finish_postfix_casts(bits_literal_to_bytea_expr(&body)));
22880        }
22881        let bits = if is_hex {
22882            let mut out = String::with_capacity(body.len() * 4);
22883            for c in body.chars() {
22884                let Some(d) = c.to_digit(16) else {
22885                    // v7.39.3 — PostgreSQL 18.6's own sentence, and its
22886                    // own quoting: `"g" is not a valid hexadecimal
22887                    // digit` (measured, with the caret on the literal).
22888                    return Some(Err(self.err_at(
22889                        lit_pos,
22890                        alloc::format!("\"{c}\" is not a valid hexadecimal digit"),
22891                    )));
22892                };
22893                out.push_str(&alloc::format!("{d:04b}"));
22894            }
22895            out
22896        } else {
22897            if let Some(bad) = body.chars().find(|c| *c != '0' && *c != '1') {
22898                return Some(Err(self.err_at(
22899                    lit_pos,
22900                    alloc::format!("\"{bad}\" is not a valid binary digit"),
22901                )));
22902            }
22903            body
22904        };
22905        // Route through the postfix-cast loop so a chained cast like
22906        // `B'1010'::int` attaches onto the implicit `::bit` cast instead
22907        // of erroring at the `::`.
22908        // v7.39 (read01 varbit.c) — a distinct internal target: a B'...'
22909        // literal keeps its exact length, while an explicit `::bit` cast is
22910        // bit(1) with pad/truncate semantics (PG).
22911        Some(self.finish_postfix_casts(Expr::Cast {
22912            expr: Box::new(Expr::Literal(Literal::String(bits))),
22913            target: CastTarget::Named("__bit_literal".to_string()),
22914        }))
22915    }
22916
22917    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
22918        if let Some(res) = self.try_parse_bit_string_literal() {
22919            return res;
22920        }
22921        // v7.40.11 — `<alias>.*` in an EXPRESSION is the whole row, the
22922        // same thing the bare alias is.
22923        //
22924        // Reported against 7.40.9: `to_jsonb(t.*)`, `row_to_json(t.*)`,
22925        // `pg_column_size(t.*)` and `count(t.*)` were all
22926        // `syntax error at or near "*"`, while `to_jsonb(t)` — the same
22927        // value, differently spelled — worked. Measured on PG 18.6, the
22928        // two spellings answer byte-identically:
22929        //
22930        //   to_jsonb(t.*)   {"a": 1, "b": "x"}
22931        //   to_jsonb(t)     {"a": 1, "b": "x"}
22932        //
22933        // Here rather than in the argument list: nothing else in an
22934        // expression position is `ident . *`, and a select item's own
22935        // `t.*` is recognised before `parse_expr` is ever called, so
22936        // `SELECT t.* FROM t` keeps expanding to the column list.
22937        if let Token::Ident(q) | Token::QuotedIdent(q) = self.peek().clone()
22938            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
22939            && matches!(self.tokens.get(self.pos + 2), Some(Token::Star))
22940        {
22941            self.advance();
22942            self.advance();
22943            self.advance();
22944            return Ok(Expr::Column(ColumnName {
22945                qualifier: None,
22946                name: q,
22947            }));
22948        }
22949        let tok_pos = self.pos;
22950        match self.advance() {
22951            Token::Integer(n) => Ok(Expr::Literal(Literal::Integer(n))),
22952            Token::Float(x) => Ok(Expr::Literal(Literal::Float(x))),
22953            // v7.38 (read01) — dotted / over-i64 literal → exact NUMERIC (PG),
22954            // carrying the source mantissa + scale so no precision is lost. A
22955            // literal too wide for i128 falls back to double precision.
22956            // Out-of-line (#[inline(never)]) — this arm sits on the
22957            // parse_expr recursion chain; its expansion locals must not
22958            // widen the recursive frame (debug frame-cliff discipline).
22959            Token::Numeric(s) => match numeric_token_to_literal(s) {
22960                Ok(lit) => Ok(Expr::Literal(lit)),
22961                Err(msg) => Err(self.err(msg)),
22962            },
22963            Token::String(s) => Ok(Expr::Literal(Literal::String(s))),
22964            // v7.39 (round 367, M20) — a MySQL `0x…` binary-string literal
22965            // (the lexer only emits this token in the MySQL dialect). Lower
22966            // onto the existing bytea cast; out-of-line to keep this arm off
22967            // the parse recursion frame.
22968            Token::HexBytes(s) => Ok(hex_literal_to_bytea_expr(&s)),
22969            Token::True => Ok(Expr::Literal(Literal::Bool(true))),
22970            Token::False => Ok(Expr::Literal(Literal::Bool(false))),
22971            Token::Null => Ok(Expr::Literal(Literal::Null)),
22972            // v6.1.1 — `$N` placeholder. The actual Value lookup
22973            // happens in the engine eval path against the prepared-
22974            // statement bind buffer.
22975            Token::Placeholder(n) => Ok(Expr::Placeholder(n)),
22976            Token::LParen => {
22977                // v4.10: `(SELECT ...)` in expression position is a
22978                // scalar subquery; otherwise it's a parenthesised
22979                // expression. Peek for SELECT keyword to dispatch.
22980                // v7.37 D.43 — also accept `(WITH [RECURSIVE] … SELECT …)`; WITH
22981                // lexes as Ident("with") (not a reserved token). The subquery body
22982                // is parsed in `parse_paren_scalar_subquery` (marked #[inline(never)]
22983                // so its large `Statement` local stays out of parse_atom's stack
22984                // frame — parse_atom is on the recursive `((…))` cycle and the
22985                // nesting budget is tuned to its frame size).
22986                let is_with = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
22987                    if s.eq_ignore_ascii_case("with"));
22988                if matches!(self.peek(), Token::Select) || is_with {
22989                    self.parse_paren_scalar_subquery(is_with)
22990                } else {
22991                    let e = self.parse_expr(0)?;
22992                    // `(a, b, …)` — a row constructor. Valid only
22993                    // in front of a comparison operator or [NOT]
22994                    // IN; both expand at parse time (lexicographic
22995                    // comparison / OR'd row equalities).
22996                    if matches!(self.peek(), Token::Comma) {
22997                        let mut row = alloc::vec![e];
22998                        while matches!(self.peek(), Token::Comma) {
22999                            self.advance();
23000                            row.push(self.parse_expr(0)?);
23001                        }
23002                        if !matches!(self.peek(), Token::RParen) {
23003                            return Err(self.err(alloc::format!(
23004                                "expected ')' after row constructor, got {:?}",
23005                                self.peek()
23006                            )));
23007                        }
23008                        self.advance();
23009                        // A bare `(a, b, …)` row constructor can carry postfix
23010                        // (`::text`, `.field`) just like `ROW(a, b, …)`; the
23011                        // early return here skips parse_atom's tail postfix
23012                        // pass, so fold casts in explicitly. For the
23013                        // comparison / predicate forms nothing postfix follows,
23014                        // so this is a no-op.
23015                        return self
23016                            .parse_row_comparison_tail(row)
23017                            .and_then(|e| self.finish_postfix_casts(e));
23018                    }
23019                    match self.advance() {
23020                        Token::RParen => Ok(e),
23021                        other => Err(ParseError {
23022                            message: format!("expected ')', got {other:?}"),
23023                            token_pos: self.consumed_pos(),
23024                        }),
23025                    }
23026                }
23027            }
23028            Token::LBracket => self.parse_vector_literal_body(),
23029            Token::Extract => self.parse_extract_atom(),
23030            Token::Interval => self.parse_interval_atom(),
23031            // `LEFT` / `RIGHT` are reserved-keyword tokens because the
23032            // grammar dedicates arms for `LEFT [OUTER] JOIN` /
23033            // `RIGHT [OUTER] JOIN`. When followed by `(` we're in
23034            // expression position calling the PG `left(string, n)` /
23035            // `right(string, n)` function; rebuild the AST as a regular
23036            // function call so the engine's apply_function dispatch picks
23037            // it up. Delegated to a #[inline(never)] helper so its locals
23038            // don't bloat this recursive `parse_atom` frame (the nesting
23039            // budget in `enter_nested` is tuned to parse_atom's size).
23040            Token::Left if matches!(self.peek(), Token::LParen) => {
23041                self.parse_lr_string_function_call("left")
23042            }
23043            Token::Right if matches!(self.peek(), Token::LParen) => {
23044                self.parse_lr_string_function_call("right")
23045            }
23046            // v4.10: EXISTS / NOT EXISTS. EXISTS isn't a reserved
23047            // token; we match on the bare ident. NOT is a token
23048            // (consumed in the comparison rung), but `EXISTS (...)`
23049            // at the top of an expression starts here.
23050            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("exists") => {
23051                self.parse_exists_atom(false)
23052            }
23053            // v7.13.0 — `CASE [<operand>] WHEN <cond> THEN <val>
23054            // [WHEN ...] [ELSE <val>] END` (mailrs round-5 G9).
23055            // CASE is a bare ident; we dispatch on lowercase match.
23056            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("case") => {
23057                self.parse_case_atom()
23058            }
23059            // v7.37.17 (17.6 siblings) — PG typed datetime literals:
23060            // `DATE '2003-01-02'` / `TIMESTAMP '…'` / `TIMESTAMPTZ
23061            // '…'`. Lower onto the ::cast node so the existing
23062            // runtime text→date/timestamp paths do the parsing. The
23063            // string must follow immediately, else the ident stays a
23064            // plain column reference.
23065            Token::Ident(s)
23066                if typed_literal_cast_target(&s.to_ascii_lowercase()).is_some()
23067                    && matches!(self.peek(), Token::String(_)) =>
23068            {
23069                let target =
23070                    typed_literal_cast_target(&s.to_ascii_lowercase()).expect("guard checked");
23071                let Token::String(lit) = self.advance() else {
23072                    unreachable!("peek guaranteed a string token");
23073                };
23074                Ok(Expr::Cast {
23075                    expr: Box::new(Expr::Literal(Literal::String(lit))),
23076                    target,
23077                })
23078            }
23079            // v7.39 (round 221) — the SQL-standard long spellings:
23080            // `TIME [WITHOUT|WITH] TIME ZONE '…'` / `TIMESTAMP [WITHOUT|WITH]
23081            // TIME ZONE '…'`. Consume the modifier and lower to the same
23082            // typed-literal cast (`timetz` / `timestamptz` for WITH).
23083            Token::Ident(s)
23084                if (s.eq_ignore_ascii_case("time") || s.eq_ignore_ascii_case("timestamp"))
23085                    && matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with")
23086                        || w.eq_ignore_ascii_case("without"))
23087                    && matches!(self.tokens.get(self.pos + 1), Some(Token::Ident(t)) if t.eq_ignore_ascii_case("time"))
23088                    && matches!(self.tokens.get(self.pos + 2), Some(Token::Ident(z)) if z.eq_ignore_ascii_case("zone"))
23089                    && matches!(self.tokens.get(self.pos + 3), Some(Token::String(_))) =>
23090            {
23091                let with_tz = matches!(self.peek(), Token::Ident(w) if w.eq_ignore_ascii_case("with"));
23092                self.advance(); // WITH / WITHOUT
23093                self.advance(); // TIME
23094                self.advance(); // ZONE
23095                let Token::String(lit) = self.advance() else {
23096                    unreachable!("guard checked a string token");
23097                };
23098                let base = s.to_ascii_lowercase();
23099                let target = match (base.as_str(), with_tz) {
23100                    ("time", true) => CastTarget::Named(alloc::string::String::from("timetz")),
23101                    ("time", false) => CastTarget::Named(alloc::string::String::from("time")),
23102                    (_, true) => CastTarget::Timestamptz,
23103                    (_, false) => CastTarget::Timestamp,
23104                };
23105                Ok(Expr::Cast {
23106                    expr: Box::new(Expr::Literal(Literal::String(lit))),
23107                    target,
23108                })
23109            }
23110            // v7.39 (read01 round 105) — `ARRAY(<subquery>)` constructor:
23111            // gathers the subquery's single-column rows (in its row order)
23112            // into an array. Desugared to `array_agg` over the subquery as a
23113            // derived table; out-of-line to keep parse_atom's frame small (it
23114            // sits on the recursive nesting-budget cycle).
23115            Token::Ident(s) | Token::QuotedIdent(s)
23116                if s.eq_ignore_ascii_case("array") && self.array_subquery_ahead() =>
23117            {
23118                self.parse_array_subquery()
23119            }
23120            // v7.10.10 — `ARRAY[expr, expr, …]` constructor. ARRAY
23121            // is not a reserved token; we match by case-insensitive
23122            // ident. The opening `[` must follow immediately. v7.39 (read01
23123            // round 105) — the body moved out-of-line so its `Vec`/loop locals
23124            // leave parse_atom's frame (which sits on the nesting-budget cycle).
23125            Token::Ident(s) | Token::QuotedIdent(s)
23126                if s.eq_ignore_ascii_case("array") && matches!(self.peek(), Token::LBracket) =>
23127            {
23128                self.parse_array_literal_body()
23129            }
23130            // v7.17.0 Phase 2.2 — MySQL `MATCH(col, ...) AGAINST
23131            // ('term' [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE])`.
23132            // We special-case before the generic ident dispatch so
23133            // the AGAINST clause never reaches the function-call
23134            // loop (which would mis-read `(cols) AGAINST` as a
23135            // call with no trailing modifier). The shape is
23136            // rewritten to a Boolean OR over per-column
23137            // `to_tsvector('simple', col) @@ plainto_tsquery('simple',
23138            // term)` so the existing FTS evaluator handles
23139            // semantics — the fulltext-GIN built at CREATE TABLE
23140            // time is currently a "real index that survives dump
23141            // round-trip"; the planner hook that actually uses
23142            // it for posting-list intersection lands in a later
23143            // sub-phase (Phase 2.2b) without touching this surface.
23144            Token::Ident(s) | Token::QuotedIdent(s)
23145                if s.eq_ignore_ascii_case("match") && matches!(self.peek(), Token::LParen) =>
23146            {
23147                self.parse_match_against_atom()
23148            }
23149            Token::Ident(s) | Token::QuotedIdent(s) => self.finish_ident_atom(s),
23150            // v7.37.43-T4 — PG-unreserved keywords are legal column /
23151            // alias names in expression context too. `release` appears
23152            // in sentori `0003_partition_events.sql` as both a column
23153            // reference (SELECT … release …) and an INSERT column list
23154            // entry. Mirrors `expect_ident_like`'s expansion of the
23155            // identifier set.
23156            other if unreserved_keyword_text(&other).is_some() => {
23157                let s = unreserved_keyword_text(&other).unwrap();
23158                self.finish_ident_atom(s)
23159            }
23160            // v7.39 (round 331, V50) — `@@var` in an EXPRESSION. It parsed
23161            // only inside `SET` before, so `SELECT @@autocommit` — which
23162            // every MySQL connector asks at handshake — was a parse error.
23163            // MariaDB accepts the bare, `@@session.` and `@@global.`
23164            // spellings alike and answers from the session's own value.
23165            Token::SessionVar(v) => {
23166                // v7.39 (round 430) — ONE `@` is a MySQL USER variable, which
23167                // has nothing to do with a `@@` engine setting: its own
23168                // per-session namespace, and an unset one reads NULL instead
23169                // of raising. Stripping every `@` (as this did) made `@x` and
23170                // `@@x` the same node, so `SELECT @x` answered "Unknown
23171                // system variable".
23172                Ok(variable_ref_atom(&v))
23173            }
23174            other => Err(ParseError {
23175                message: format!("unexpected token {other:?} in expression"),
23176                token_pos: tok_pos,
23177            }),
23178        }
23179        // After parsing the atom, fold any postfix `::vector` casts.
23180        .and_then(|atom| self.finish_postfix_casts(atom))
23181    }
23182
23183    /// Postfix operators on an atom: `::TYPE` cast and `IS [NOT] NULL`.
23184    /// Both bind tighter than any binary op.
23185    /// Shared cast-target parser for postfix `::TYPE` and the
23186    /// standard `CAST(expr AS TYPE)` form (v7.25, round-17).
23187    /// If the next tokens are `( N )`, consume them and return the canonical
23188    /// `base(N)` name so a temporal cast (`::timestamp(2)`) carries its
23189    /// fractional-seconds precision into `CastTarget::Named`; otherwise `None`.
23190    fn consume_temporal_typmod(&mut self, base: &str) -> Option<alloc::string::String> {
23191        if !matches!(self.peek(), Token::LParen) {
23192            return None;
23193        }
23194        self.advance(); // (
23195        let n = match self.advance() {
23196            Token::Integer(n) => n,
23197            _ => return Some(base.to_string()), // malformed → drop precision
23198        };
23199        if matches!(self.peek(), Token::RParen) {
23200            self.advance();
23201        }
23202        Some(alloc::format!("{base}({n})"))
23203    }
23204
23205    fn parse_cast_target(&mut self) -> Result<CastTarget, ParseError> {
23206        // r1052 — `::pg_catalog.regproc` and friends: pg_dump
23207        // schema-qualifies every cast target, and `pg_catalog.X` names
23208        // exactly the builtin type X. Consume the qualifier and let
23209        // the ordinary target parse decide.
23210        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("pg_catalog"))
23211            && matches!(self.tokens.get(self.pos + 1), Some(Token::Dot))
23212        {
23213            self.advance();
23214            self.advance();
23215        }
23216        let target = match self.advance() {
23217            Token::Ident(s) => match s.to_ascii_lowercase().as_str() {
23218                "int" | "integer" | "int4" => {
23219                    if matches!(self.peek(), Token::LBracket)
23220                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23221                    {
23222                        self.advance();
23223                        self.advance();
23224                        CastTarget::IntArray
23225                    } else {
23226                        CastTarget::Int
23227                    }
23228                }
23229                "bigint" | "int8" => {
23230                    if matches!(self.peek(), Token::LBracket)
23231                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23232                    {
23233                        self.advance();
23234                        self.advance();
23235                        CastTarget::BigIntArray
23236                    } else {
23237                        CastTarget::BigInt
23238                    }
23239                }
23240                "float" | "double" => CastTarget::Float,
23241                "text" => {
23242                    // v7.10.11 — `::TEXT[]` widens to TextArray.
23243                    if matches!(self.peek(), Token::LBracket)
23244                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23245                    {
23246                        self.advance();
23247                        self.advance();
23248                        CastTarget::TextArray
23249                    } else {
23250                        CastTarget::Text
23251                    }
23252                }
23253                "bool" | "boolean" => CastTarget::Bool,
23254                "vector" => CastTarget::Vector,
23255                "date" => CastTarget::Date,
23256                // v7.38 (read01) — `::timestamp(N)` carries its fractional-
23257                // seconds precision through the Named path (the engine rounds
23258                // the sub-second field); bare `::timestamp` keeps the fast arm.
23259                "timestamp" | "datetime" => match self.consume_temporal_typmod("timestamp") {
23260                    Some(named) => CastTarget::Named(named),
23261                    None => CastTarget::Timestamp,
23262                },
23263                "timestamptz" => match self.consume_temporal_typmod("timestamptz") {
23264                    Some(named) => CastTarget::Named(named),
23265                    None => CastTarget::Timestamptz,
23266                },
23267                "interval" => CastTarget::Interval,
23268                "json" => CastTarget::Json,
23269                "jsonb" => CastTarget::Jsonb,
23270                // v7.39 (round 694) — these have dedicated CastTarget
23271                // variants, so they never reached the postfix `[]` handling
23272                // further down and `::regtype[]` was a SYNTAX error at the
23273                // `]`. PG has an array type for every scalar; take the
23274                // suffix here and hand the canonical `<ty>_array` name to
23275                // the engine, the same shape every other array cast uses.
23276                "regtype" if self.peek_postfix_array_brackets() => {
23277                    self.advance();
23278                    self.advance();
23279                    CastTarget::Named(alloc::string::String::from("regtype_array"))
23280                }
23281                "regclass" if self.peek_postfix_array_brackets() => {
23282                    self.advance();
23283                    self.advance();
23284                    CastTarget::Named(alloc::string::String::from("regclass_array"))
23285                }
23286                "regtype" => CastTarget::RegType,
23287                "regclass" => CastTarget::RegClass,
23288                // v7.12.0 — `::tsvector` / `::tsquery`.
23289                // Engine decodes the LHS text via the PG
23290                // external form parser.
23291                // v7.39 (round 352, M8) — MySQL's own cast targets.
23292                // `CAST(x AS SIGNED)` / `UNSIGNED`, with the optional
23293                // `INTEGER` / `INT` tail MariaDB also accepts. PG has no
23294                // such type, so they are taken only in that dialect and
23295                // fall through to the "type does not exist" arm otherwise.
23296                "signed" | "unsigned" if self.mysql_dialect => {
23297                    if matches!(self.peek(), Token::Ident(k)
23298                        if k.eq_ignore_ascii_case("integer") || k.eq_ignore_ascii_case("int"))
23299                    {
23300                        self.advance();
23301                    }
23302                    CastTarget::Named(s.to_ascii_lowercase())
23303                }
23304                // v7.39 (round 352, M8) — `CAST(x AS CHAR)` is UNBOUNDED
23305                // in MySQL: MariaDB answers '123' where the SQL-standard
23306                // reading (PG's, and SPG's) is `char(1)` and answers '1'.
23307                // Truncating a number to its first digit is a wrong answer
23308                // with no error, so the MySQL session gets MySQL's reading.
23309                "char" if self.mysql_dialect && !matches!(self.peek(), Token::LParen) => {
23310                    CastTarget::Text
23311                }
23312                "tsvector" => CastTarget::TsVector,
23313                "tsquery" => CastTarget::TsQuery,
23314                // v7.17.0 — `::uuid`. Engine decodes the LHS
23315                // text via `spg_storage::parse_uuid_str`.
23316                "uuid" => CastTarget::Uuid,
23317                // v7.18 — `::bytea`. Engine decodes the LHS
23318                // text via the PG hex form (`'\xdeadbeef'`)
23319                // or escape form (`'\\x05\\x00'`). Closes
23320                // mailrs D-pre #3 reverse-acceptance gap.
23321                "bytea" => CastTarget::Bytea,
23322                // v7.37.5 ship triage — generic typed-cast escape.
23323                // Anything the long-tail PG type ident table knows
23324                // about(network/bit/geometry/multirange/etc.)flows
23325                // through `CastTarget::Named(canonical)`; the engine
23326                // resolves via `column_type_to_data_type` and dispatches
23327                // through the typed `coerce_value` path. Truly
23328                // unrecognised idents still hit the error arm below
23329                // because the engine rejects them.
23330                other => {
23331                    // Optional `(N[, M])` precision args — `::numeric(10,2)`,
23332                    // `::varchar(255)`, etc. Capture into the canonical
23333                    // `name(p,s)` form so `type_name_to_data_type` can
23334                    // reconstruct the `DataType::Numeric { precision,
23335                    // scale }` (and similar param-carrying types).
23336                    let mut name = other.to_string();
23337                    // v7.39 (round 281) — `::bit varying(3)` is two
23338                    // words; fold the tail in so the typmod reaches the
23339                    // type resolver instead of tripping the parser.
23340                    if name.eq_ignore_ascii_case("bit")
23341                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23342                    {
23343                        self.advance();
23344                        name = alloc::string::String::from("varbit");
23345                    }
23346                    // v7.39 (round 613) — `::character varying` is the same
23347                    // two-word shape and had no fold, so the `varying` was
23348                    // left behind and the cast became a bare `character`,
23349                    // which is `char(1)`: `'ab'::CHARACTER VARYING` answered
23350                    // `a` where PG answers `ab`. Silently, and for a spelling
23351                    // pg_dump writes.
23352                    if name.eq_ignore_ascii_case("character")
23353                        && matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("varying"))
23354                    {
23355                        self.advance();
23356                        name = alloc::string::String::from("varchar");
23357                    }
23358                    if matches!(self.peek(), Token::LParen) {
23359                        let mut buf = alloc::string::String::from("(");
23360                        let mut depth = 0usize;
23361                        loop {
23362                            match self.advance() {
23363                                Token::LParen => {
23364                                    depth += 1;
23365                                    if depth > 1 {
23366                                        buf.push('(');
23367                                    }
23368                                }
23369                                Token::RParen => {
23370                                    depth -= 1;
23371                                    if depth == 0 {
23372                                        buf.push(')');
23373                                        break;
23374                                    }
23375                                    buf.push(')');
23376                                }
23377                                Token::Comma => buf.push(','),
23378                                Token::Integer(n) => buf.push_str(&alloc::format!("{n}")),
23379                                // v7.39 (round 273) — a minus used to fall
23380                                // into the catch-all below and vanish, so
23381                                // `::numeric(10,-2)` reached the engine as
23382                                // the text `numeric(10,2)` and silently
23383                                // rounded to two DECIMALS instead of to
23384                                // hundreds. A dropped token is not a
23385                                // no-op when it carries a sign.
23386                                Token::Minus => buf.push('-'),
23387                                Token::Eof => break,
23388                                _ => {}
23389                            }
23390                        }
23391                        name.push_str(&buf);
23392                    }
23393                    // Optional postfix `[]` widens to the array form —
23394                    // `::BOOL[]`, `::NUMERIC[]`, `::SMALLINT[]`, etc.
23395                    // The engine's `type_name_to_data_type` recognises
23396                    // the canonical `<ty>_array` form.
23397                    if matches!(self.peek(), Token::LBracket)
23398                        && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23399                    {
23400                        self.advance();
23401                        self.advance();
23402                        name.push_str("_array");
23403                    }
23404                    CastTarget::Named(name)
23405                }
23406            },
23407            Token::Interval => CastTarget::Interval,
23408            // v7.39 — a quoted type name: `::"char"` is PG's 1-byte
23409            // "char" (oid 18, SPG Char1 — distinct from bare `char`
23410            // = char(1)); other quoted names resolve like idents.
23411            Token::QuotedIdent(q) => {
23412                if q.eq_ignore_ascii_case("char") {
23413                    CastTarget::Named("char1".into())
23414                } else {
23415                    CastTarget::Named(q.to_ascii_lowercase())
23416                }
23417            }
23418            other => {
23419                return Err(ParseError {
23420                    message: format!("expected type ident after `::`, got {other:?}"),
23421                    token_pos: self.consumed_pos(),
23422                });
23423            }
23424        };
23425        // v7.37.5 ship triage — postfix `[]` widens a scalar cast
23426        // target to its array sibling. Closed-enum arms (Bool /
23427        // SmallInt / Numeric / Float / Date / …) didn't carry the
23428        // explicit widening that Text / Int / BigInt did, so
23429        // `::BOOL[]` / `::NUMERIC[]` etc. surfaced as a parse
23430        // error. The widening here mirrors the per-arm Text /
23431        // Int / BigInt logic above + folds the new ζ-A first-class
23432        // types through `CastTarget::Named("<ty>_array")`.
23433        if matches!(self.peek(), Token::LBracket)
23434            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23435        {
23436            let widened = match &target {
23437                CastTarget::Bool => Some(CastTarget::Named("bool_array".to_string())),
23438                CastTarget::Date => Some(CastTarget::Named("date_array".to_string())),
23439                // v7.39 (round 326, V43) — the two temporal types stay
23440                // distinct. Both used to widen to `timestamptz_array`, so
23441                // `::timestamp[]` named the wrong target in its own error
23442                // message and lost the zone-less identity on the way.
23443                CastTarget::Timestamp => Some(CastTarget::Named("timestamp_array".to_string())),
23444                CastTarget::Timestamptz => Some(CastTarget::Named("timestamptz_array".to_string())),
23445                CastTarget::Uuid => Some(CastTarget::Named("uuid_array".to_string())),
23446                CastTarget::Json | CastTarget::Jsonb => {
23447                    Some(CastTarget::Named("jsonb_array".to_string()))
23448                }
23449                CastTarget::Bytea => Some(CastTarget::Named("bytea_array".to_string())),
23450                CastTarget::Interval => Some(CastTarget::Named("interval_array".to_string())),
23451                CastTarget::Float => Some(CastTarget::Named("float_array".to_string())),
23452                CastTarget::Named(name) => {
23453                    let mut a = name.clone();
23454                    a.push_str("_array");
23455                    Some(CastTarget::Named(a))
23456                }
23457                // Int / BigInt / Text / Vector / TsVector / TsQuery /
23458                // RegType / RegClass / TextArray / IntArray /
23459                // BigIntArray already finalised — leave as is.
23460                _ => None,
23461            };
23462            if let Some(w) = widened {
23463                self.advance();
23464                self.advance();
23465                return Ok(w);
23466            }
23467        }
23468        Ok(target)
23469    }
23470
23471    fn finish_postfix_casts(&mut self, mut expr: Expr) -> Result<Expr, ParseError> {
23472        loop {
23473            // v7.38 (read01, T9) — composite field access `(expr).field`.
23474            // A bare `a.b` is consumed as a qualified column inside the ident
23475            // atom, so a Dot only survives to this postfix position when the
23476            // base was a parenthesised expression (`(e).id`, `(row(1,2)).f1`).
23477            // `.*` whole-row expansion is not handled here (projection-level).
23478            if matches!(self.peek(), Token::Dot)
23479                && matches!(
23480                    self.tokens.get(self.pos + 1),
23481                    Some(Token::Ident(_) | Token::QuotedIdent(_))
23482                )
23483            {
23484                self.advance(); // .
23485                let field = match self.advance() {
23486                    Token::Ident(s) | Token::QuotedIdent(s) => s,
23487                    other => {
23488                        return Err(
23489                            self.err(format!("expected a field name after '.', got {other:?}"))
23490                        );
23491                    }
23492                };
23493                expr = Expr::FieldAccess {
23494                    base: Box::new(expr),
23495                    field,
23496                };
23497                continue;
23498            }
23499            if matches!(self.peek(), Token::DoubleColon) {
23500                self.advance();
23501                // v7.9.25 / v7.9.26 — broaden the postfix `::` cast
23502                // target set to include INTERVAL (reserved Token),
23503                // TIMESTAMPTZ, and PG catalog regtype / regclass.
23504                // mailrs follow-up H3a + H3b.
23505                let target = self.parse_cast_target()?;
23506                expr = Expr::Cast {
23507                    expr: Box::new(expr),
23508                    target,
23509                };
23510                continue;
23511            }
23512            // v7.10.12 — `arr[i]` subscript. PG 1-based; engine
23513            // returns NULL for out-of-range. Multiple subscripts
23514            // chain: `a[i][j]` parses left-to-right.
23515            if matches!(self.peek(), Token::LBracket) {
23516                self.advance();
23517                // `[lo:hi]` / `[:hi]` / `[lo:]` — array slice. A
23518                // bare index stays a subscript.
23519                let lo = if matches!(self.peek(), Token::Colon) {
23520                    None
23521                } else {
23522                    Some(self.parse_expr(0)?)
23523                };
23524                if matches!(self.peek(), Token::Colon) {
23525                    self.advance();
23526                    let hi = if matches!(self.peek(), Token::RBracket) {
23527                        None
23528                    } else {
23529                        Some(Box::new(self.parse_expr(0)?))
23530                    };
23531                    if !matches!(self.peek(), Token::RBracket) {
23532                        return Err(self.err(alloc::format!(
23533                            "expected ']' after array slice, got {:?}",
23534                            self.peek()
23535                        )));
23536                    }
23537                    self.advance();
23538                    expr = Expr::ArraySlice {
23539                        target: Box::new(expr),
23540                        lo: lo.map(Box::new),
23541                        hi,
23542                    };
23543                    continue;
23544                }
23545                let index = lo.expect("non-colon branch parsed an index");
23546                if !matches!(self.peek(), Token::RBracket) {
23547                    return Err(self.err(alloc::format!(
23548                        "expected ']' after array index, got {:?}",
23549                        self.peek()
23550                    )));
23551                }
23552                self.advance();
23553                expr = Expr::ArraySubscript {
23554                    target: Box::new(expr),
23555                    index: Box::new(index),
23556                };
23557                continue;
23558            }
23559            // `expr AT TIME ZONE zone` — lowers to PG's own function
23560            // form timezone(zone, expr); the scalar implements the
23561            // offset shift (named zones error there — no tzdata).
23562            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("at"))
23563                && matches!(self.tokens.get(self.pos + 1),
23564                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("time"))
23565                && matches!(self.tokens.get(self.pos + 2),
23566                    Some(Token::Ident(s)) if s.eq_ignore_ascii_case("zone"))
23567            {
23568                self.advance(); // AT
23569                self.advance(); // TIME
23570                self.advance(); // ZONE
23571                // Zone at comparison precedence so AND/OR stay out.
23572                let zone = self.parse_expr(6)?;
23573                expr = Expr::FunctionCall {
23574                    name: "timezone".to_string(),
23575                    args: alloc::vec![zone, expr],
23576                };
23577                continue;
23578            }
23579            // `expr COLLATE "name"` — SPG's single text ordering IS
23580            // byte order, i.e. the C collation. The byte-order
23581            // spellings absorb as no-ops; a locale collation would
23582            // silently sort differently from PG, so it errors
23583            // honestly instead.
23584            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("collate")) {
23585                self.advance();
23586                let mut cname = match self.advance() {
23587                    Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23588                    other => {
23589                        return Err(self.err(alloc::format!(
23590                            "expected collation name after COLLATE, got {other:?}"
23591                        )));
23592                    }
23593                };
23594                // v7.39 (round 539) — a SCHEMA-QUALIFIED collation, which
23595                // is how `pg_dump` writes the default one:
23596                // `… COLLATE pg_catalog.default`. Reading a single token
23597                // left the SCHEMA as the name, so the clause was refused
23598                // as an unsupported locale collation and no dump ran.
23599                if matches!(self.peek(), Token::Dot) {
23600                    // v7.39.2 — the qualifier is DROPPED (SPG is single
23601                    // schema) but it is checked first. PostgreSQL 18.6
23602                    // answers `schema "nosuch_schema" does not exist` for
23603                    // one it has never heard of, and dropping it unread
23604                    // meant `COLLATE nosuch_schema."C"` succeeded here —
23605                    // a name that names nothing, accepted.
23606                    let schema = cname.to_ascii_lowercase();
23607                    if !matches!(
23608                        schema.as_str(),
23609                        "pg_catalog" | "public" | "information_schema"
23610                    ) {
23611                        return Err(self.err(alloc::format!("schema \"{cname}\" does not exist")));
23612                    }
23613                    self.advance();
23614                    cname = match self.advance() {
23615                        Token::Ident(s) | Token::QuotedIdent(s) | Token::String(s) => s,
23616                        // `default` lexes as a KEYWORD, and it is the name
23617                        // pg_dump writes — the same trap round 535 hit with
23618                        // TABLE / INDEX / FULL.
23619                        Token::Default => alloc::string::String::from("default"),
23620                        other => {
23621                            return Err(self.err(alloc::format!(
23622                                "expected collation name after COLLATE, got {other:?}"
23623                            )));
23624                        }
23625                    };
23626                }
23627                let lc = cname.to_ascii_lowercase();
23628                // v7.39 (round 371, M4 P4b) — a per-expression MySQL
23629                // collation override. `… COLLATE utf8mb4_bin` (any `_bin`
23630                // family / `binary`) forces byte-wise, which is exactly
23631                // what `BINARY expr` does — lower onto that so every fold
23632                // site (comparison, LIKE, ORDER BY) suppresses via
23633                // `is_binary_coerced`. A `_ci` family override folds, and
23634                // under the MySQL dialect the default already folds, so it
23635                // absorbs as a no-op; likewise the C / byte-order spellings.
23636                // v7.39.2 — against MySQL's own list, not against the
23637                // shape of the name. `nosuch_bin` took this shortcut and
23638                // became a BINARY cast; `nosuch_ci` took the one below
23639                // and was absorbed as a no-op. Either way the client
23640                // named a collation that does not exist and was told
23641                // nothing. An unknown name now falls through to the
23642                // node, and the engine refuses it.
23643                let real = crate::charset::is_mysql_collation(&lc);
23644                // v7.40.0 — `binary` lowers; a `_bin` COLLATION does not.
23645                //
23646                // They are not the same thing, and folding them together
23647                // lost a bit. Measured on MySQL 9.7.2 with the connection
23648                // on utf8mb4:
23649                //
23650                // ```text
23651                //   'a ' = 'a' COLLATE utf8mb4_bin        1   PAD SPACE
23652                //   'a ' = 'a' COLLATE utf8mb4_0900_bin   0   NO PAD
23653                //   'AB' = 'ab' COLLATE utf8mb4_bin       0   byte-wise
23654                // ```
23655                //
23656                // The BINARY cast carries "byte-wise" and, with it,
23657                // "no pad" — so `utf8mb4_bin`, which pads, answered 0 to
23658                // the first line. Keeping the node lets `text_compare_of`
23659                // read the NAME and settle the two bits separately: it
23660                // does not fold (`folds_case` says so) and it does pad
23661                // (`pads_space` says so), while `is_byte_wise` still
23662                // keeps the ORDERING off the locale.
23663                if self.mysql_dialect && real && lc == "binary" {
23664                    expr = Expr::Cast {
23665                        expr: alloc::boxed::Box::new(expr),
23666                        target: CastTarget::Named("binary".to_string()),
23667                    };
23668                    continue;
23669                }
23670                let mysql_ci = self.mysql_dialect
23671                    && ((real && lc.ends_with("_ci"))
23672                        || matches!(lc.as_str(), "case_insensitive" | "nocase"));
23673                // v7.39 (round 691/692) — inside an ORDER BY key EVERY name
23674                // goes to the lowering channel, the byte-order spellings
23675                // included. Round 691 recorded only the names the old
23676                // allow-list rejected, which left `ORDER BY a COLLATE "C"`
23677                // absorbed as a no-op — and once a column could declare a
23678                // collation, absorbing the clause meant the COLUMN's
23679                // collation won where the query had asked for bytes.
23680                if self.in_order_by_key && !mysql_ci {
23681                    self.order_key_collation = Some(cname);
23682                    continue;
23683                }
23684                // v7.39.2 — the clause becomes a NODE rather than being
23685                // refused or absorbed.
23686                //
23687                // What stood here refused the locale names and SILENTLY
23688                // DROPPED the byte-order ones, so `'a' COLLATE "C" < 'B'`
23689                // answered `t` where PostgreSQL 18.6 answers `f`: the one
23690                // family it let through is the one where dropping it
23691                // changes the answer. Absorbing is only correct when the
23692                // collation asked for is the one the comparison would use
23693                // anyway, and that depends on the DATABASE — which the
23694                // parser cannot see. So it rides along and the engine,
23695                // which can, decides.
23696                //
23697                // `collate_derive` already modelled `Explicit(name)` and
23698                // had no way to be handed one.
23699                // v7.39.2 — a MySQL spelling does not exist on the
23700                // PostgreSQL wire, and THIS is where the wire is known.
23701                //
23702                // The check lived in the evaluator first and asked
23703                // `ctx.mysql_dialect`, which the INSERT path builds as a
23704                // hard-coded `false` — so `INSERT … VALUES (_utf8mb4'x')`
23705                // in a MySQL session was refused for a collation that
23706                // does not exist on a wire it was not on. Making that
23707                // context truthful would change INSERT-time evaluation
23708                // in other ways as a side effect; the parser already
23709                // gates the introducer on the same flag and is the
23710                // honest place to ask.
23711                if !self.mysql_dialect
23712                    && (lc.ends_with("_ci")
23713                        || lc.ends_with("_cs")
23714                        || lc.ends_with("_bin")
23715                        || lc == "binary"
23716                        || matches!(lc.as_str(), "case_insensitive" | "nocase"))
23717                {
23718                    return Err(self.err(alloc::format!(
23719                        "collation \"{cname}\" for encoding \"UTF8\" does not exist"
23720                    )));
23721                }
23722                // v7.39.3 — the node is built for EVERY name, `_ci`
23723                // included.
23724                //
23725                // A MySQL `_ci` spelling used to be absorbed here on the
23726                // reasoning that a MySQL session folds anyway, so the
23727                // clause asked for what it would have got. That stopped
23728                // being true when the fold learned to read the session's
23729                // collation NAME: under `SET NAMES utf8mb4 COLLATE
23730                // utf8mb4_bin`, `'AB' COLLATE utf8mb4_general_ci = 'ab'`
23731                // is 1 on MySQL 9.7.2 and was 0 here, because the clause
23732                // that would have made it 1 had been dropped in the
23733                // parser. Absorbing is only ever correct when the
23734                // collation asked for is the one the comparison would use
23735                // anyway, and the parser cannot know that — the same
23736                // reasoning already written above for the byte-order
23737                // spellings, applied to the family it had exempted.
23738                expr = Expr::Collate {
23739                    expr: alloc::boxed::Box::new(expr),
23740                    collation: cname,
23741                };
23742                continue;
23743            }
23744            return Ok(expr);
23745        }
23746    }
23747
23748    /// v7.39 (round 696) — a comma-separated list of bare names, stopping at
23749    /// the first token that is not one. Schema qualifiers collapse to the
23750    /// last part, which is what every other name path here does (SPG is
23751    /// single-schema).
23752    fn take_comma_separated_names(&mut self) -> Vec<String> {
23753        let mut out = Vec::new();
23754        while let Token::Ident(n) | Token::QuotedIdent(n) = self.peek().clone() {
23755            self.advance();
23756            let mut last = n;
23757            while matches!(self.peek(), Token::Dot) {
23758                self.advance();
23759                if let Token::Ident(t) | Token::QuotedIdent(t) = self.advance() {
23760                    last = t;
23761                }
23762            }
23763            out.push(last);
23764            if matches!(self.peek(), Token::Comma) {
23765                self.advance();
23766            } else {
23767                break;
23768            }
23769        }
23770        out
23771    }
23772
23773    /// v7.39 (round 694) — is the next token pair a postfix `[]`?
23774    ///
23775    /// The general cast-target path tests this inline; the types with their
23776    /// own `CastTarget` variant need it as a guard on their match arm,
23777    /// which is what this exists for.
23778    fn peek_postfix_array_brackets(&self) -> bool {
23779        matches!(self.peek(), Token::LBracket)
23780            && matches!(self.tokens.get(self.pos + 1), Some(Token::RBracket))
23781    }
23782
23783    /// Parse the operator tail after a `(a, b, …)` row constructor
23784    /// and expand at parse time. `=` is the conjunction of element
23785    /// equalities; `<>` its negation; the order operators expand
23786    /// lexicographically; `[NOT] IN ( (row), … )` ORs the row
23787    /// equalities. Anything else (a bare row value, a subquery
23788    /// RHS) errors honestly — SPG has no composite runtime value.
23789    fn parse_row_comparison_tail(&mut self, row: Vec<Expr>) -> Result<Expr, ParseError> {
23790        fn row_eq(lhs: &[Expr], rhs: &[Expr]) -> Expr {
23791            let mut it = lhs.iter().zip(rhs.iter()).map(|(l, r)| Expr::Binary {
23792                lhs: Box::new(l.clone()),
23793                op: BinOp::Eq,
23794                rhs: Box::new(r.clone()),
23795            });
23796            let first = it.next().expect("row has at least two elements");
23797            it.fold(first, |acc, e| Expr::Binary {
23798                lhs: Box::new(acc),
23799                op: BinOp::And,
23800                rhs: Box::new(e),
23801            })
23802        }
23803        // Lexicographic (a,b) OP (c,d):
23804        //   a STRICT c OR (a = c AND (b OP d))  — recursing right.
23805        fn row_lex(lhs: &[Expr], rhs: &[Expr], strict: BinOp, last: BinOp) -> Expr {
23806            if lhs.len() == 1 {
23807                return Expr::Binary {
23808                    lhs: Box::new(lhs[0].clone()),
23809                    op: last,
23810                    rhs: Box::new(rhs[0].clone()),
23811                };
23812            }
23813            let head_strict = Expr::Binary {
23814                lhs: Box::new(lhs[0].clone()),
23815                op: strict,
23816                rhs: Box::new(rhs[0].clone()),
23817            };
23818            let head_eq = Expr::Binary {
23819                lhs: Box::new(lhs[0].clone()),
23820                op: BinOp::Eq,
23821                rhs: Box::new(rhs[0].clone()),
23822            };
23823            Expr::Binary {
23824                lhs: Box::new(head_strict),
23825                op: BinOp::Or,
23826                rhs: Box::new(Expr::Binary {
23827                    lhs: Box::new(head_eq),
23828                    op: BinOp::And,
23829                    rhs: Box::new(row_lex(&lhs[1..], &rhs[1..], strict, last)),
23830                }),
23831            }
23832        }
23833        let negated_in = if matches!(self.peek(), Token::Not)
23834            && matches!(self.tokens.get(self.pos + 1), Some(Token::In))
23835        {
23836            self.advance();
23837            true
23838        } else {
23839            false
23840        };
23841        if matches!(self.peek(), Token::In) {
23842            self.advance();
23843            if !matches!(self.peek(), Token::LParen) {
23844                return Err(self.err(alloc::format!(
23845                    "expected '(' after row IN, got {:?}",
23846                    self.peek()
23847                )));
23848            }
23849            self.advance();
23850            // `(a, b) [NOT] IN (SELECT x, y)` — a multi-column subquery,
23851            // not a list of literal rows. Row-vs-list decomposes to
23852            // OR-of-AND above, but the subquery's rows are only known at
23853            // runtime, so keep it as a RowInSubquery node.
23854            if matches!(self.peek(), Token::Select) {
23855                let inner = self.parse_select_stmt()?;
23856                if !matches!(self.peek(), Token::RParen) {
23857                    return Err(self.err(alloc::format!(
23858                        "expected ')' after row IN-subquery, got {:?}",
23859                        self.peek()
23860                    )));
23861                }
23862                self.advance();
23863                let Statement::Select(s) = inner else {
23864                    unreachable!("parse_select_stmt always returns Statement::Select")
23865                };
23866                return Ok(Expr::RowInSubquery {
23867                    row,
23868                    subquery: Box::new(s),
23869                    negated: negated_in,
23870                });
23871            }
23872            let mut alternatives: Vec<Expr> = Vec::new();
23873            loop {
23874                // Optional ROW keyword before the paren row.
23875                if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23876                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23877                {
23878                    self.advance();
23879                }
23880                if !matches!(self.peek(), Token::LParen) {
23881                    return Err(self.err(alloc::format!(
23882                        "expected '(' to open a row inside IN, got {:?}",
23883                        self.peek()
23884                    )));
23885                }
23886                self.advance();
23887                let mut rhs = alloc::vec![self.parse_expr(0)?];
23888                while matches!(self.peek(), Token::Comma) {
23889                    self.advance();
23890                    rhs.push(self.parse_expr(0)?);
23891                }
23892                if !matches!(self.peek(), Token::RParen) {
23893                    return Err(self.err(alloc::format!(
23894                        "expected ')' after row inside IN, got {:?}",
23895                        self.peek()
23896                    )));
23897                }
23898                self.advance();
23899                if rhs.len() != row.len() {
23900                    return Err(self.err(alloc::format!(
23901                        "row IN arity mismatch: left has {}, right has {}",
23902                        row.len(),
23903                        rhs.len()
23904                    )));
23905                }
23906                alternatives.push(row_eq(&row, &rhs));
23907                if matches!(self.peek(), Token::Comma) {
23908                    self.advance();
23909                    continue;
23910                }
23911                break;
23912            }
23913            if !matches!(self.peek(), Token::RParen) {
23914                return Err(self.err(alloc::format!(
23915                    "expected ')' to close row IN list, got {:?}",
23916                    self.peek()
23917                )));
23918            }
23919            self.advance();
23920            let mut it = alternatives.into_iter();
23921            let first = it.next().expect("IN list has at least one row");
23922            let combined = it.fold(first, |acc, e| Expr::Binary {
23923                lhs: Box::new(acc),
23924                op: BinOp::Or,
23925                rhs: Box::new(e),
23926            });
23927            return Ok(maybe_not(combined, negated_in));
23928        }
23929        // SQL-standard `(S1, E1) OVERLAPS (S2, E2)` — true when the
23930        // two periods share at least one time point. Each pair is
23931        // normalised with least/greatest (PG accepts the endpoints
23932        // in either order), then lowered to the standard
23933        // `start1 < end2 AND start2 < end1` form.
23934        if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps")) {
23935            if row.len() != 2 {
23936                return Err(self.err(alloc::format!(
23937                    "OVERLAPS needs (start, end) pairs; left side has {} elements",
23938                    row.len()
23939                )));
23940            }
23941            self.advance();
23942            if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
23943                && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
23944            {
23945                self.advance();
23946            }
23947            if !matches!(self.peek(), Token::LParen) {
23948                return Err(self.err(alloc::format!(
23949                    "expected '(' after OVERLAPS, got {:?}",
23950                    self.peek()
23951                )));
23952            }
23953            self.advance();
23954            let r0 = self.parse_expr(0)?;
23955            if !matches!(self.peek(), Token::Comma) {
23956                return Err(self.err(alloc::format!(
23957                    "OVERLAPS needs (start, end) on the right, got {:?}",
23958                    self.peek()
23959                )));
23960            }
23961            self.advance();
23962            let r1 = self.parse_expr(0)?;
23963            if !matches!(self.peek(), Token::RParen) {
23964                return Err(self.err(alloc::format!(
23965                    "expected ')' after OVERLAPS pair, got {:?}",
23966                    self.peek()
23967                )));
23968            }
23969            self.advance();
23970            let pair_fn = |name: &str, a: &Expr, b: &Expr| Expr::FunctionCall {
23971                name: String::from(name),
23972                args: alloc::vec![a.clone(), b.clone()],
23973            };
23974            let lt = |lhs: Expr, rhs: Expr| Expr::Binary {
23975                lhs: Box::new(lhs),
23976                op: BinOp::Lt,
23977                rhs: Box::new(rhs),
23978            };
23979            return Ok(Expr::Binary {
23980                lhs: Box::new(lt(
23981                    pair_fn("least", &row[0], &row[1]),
23982                    pair_fn("greatest", &r0, &r1),
23983                )),
23984                op: BinOp::And,
23985                rhs: Box::new(lt(
23986                    pair_fn("least", &r0, &r1),
23987                    pair_fn("greatest", &row[0], &row[1]),
23988                )),
23989            });
23990        }
23991        // `(a, b, …) IS [NOT] NULL` — the SQL row null predicate. Per
23992        // PG, `IS NULL` is true only when EVERY field is NULL, and
23993        // `IS NOT NULL` is true only when every field is non-NULL — the
23994        // latter is NOT the negation of the former (a mixed row is
23995        // neither). Desugar to an AND chain of per-field `IS [NOT] NULL`,
23996        // which reproduces exactly that all-fields semantics.
23997        if matches!(self.peek(), Token::Is) {
23998            self.advance();
23999            let negated = if matches!(self.peek(), Token::Not) {
24000                self.advance();
24001                true
24002            } else {
24003                false
24004            };
24005            if !matches!(self.peek(), Token::Null) {
24006                return Err(self.err(alloc::format!(
24007                    "expected NULL after row IS [NOT], got {:?}",
24008                    self.peek()
24009                )));
24010            }
24011            self.advance();
24012            let mut it = row.iter().map(|e| Expr::IsNull {
24013                expr: Box::new(e.clone()),
24014                negated,
24015            });
24016            let first = it.next().expect("row has at least two elements");
24017            return Ok(it.fold(first, |acc, e| Expr::Binary {
24018                lhs: Box::new(acc),
24019                op: BinOp::And,
24020                rhs: Box::new(e),
24021            }));
24022        }
24023        let op = match self.peek() {
24024            Token::Eq => BinOp::Eq,
24025            Token::NotEq => BinOp::NotEq,
24026            Token::Lt => BinOp::Lt,
24027            Token::LtEq => BinOp::LtEq,
24028            Token::Gt => BinOp::Gt,
24029            Token::GtEq => BinOp::GtEq,
24030            // v7.38 (read01, composite) — a bare `(a, b, …)` not followed by a
24031            // comparison / [NOT] IN / IS [NOT] NULL / OVERLAPS is a row (record)
24032            // constructor value, identical to the `ROW(a, b, …)` keyword form:
24033            // `(1,'a')::text` → `(1,a)`, `SELECT (1,2,3)` → `(1,2,3)`. Postfix
24034            // (`::text`, `.field`) applies at the caller just as it does for the
24035            // ROW(...) node. All the comparison / predicate forms returned above.
24036            _ => {
24037                return Ok(Expr::FunctionCall {
24038                    name: String::from("row"),
24039                    args: row,
24040                });
24041            }
24042        };
24043        self.advance();
24044        // Optional ROW keyword before the paren row.
24045        if matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("row"))
24046            && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen))
24047        {
24048            self.advance();
24049        }
24050        if !matches!(self.peek(), Token::LParen) {
24051            return Err(self.err(alloc::format!(
24052                "expected '(' to open the right-hand row, got {:?}",
24053                self.peek()
24054            )));
24055        }
24056        self.advance();
24057        // `(a, b) <op> (SELECT x, y)` — compare against a single-row
24058        // subquery. Kept as a node (the subquery's row is a runtime value);
24059        // the literal-RHS form below still decomposes at parse time.
24060        if matches!(self.peek(), Token::Select) {
24061            let inner = self.parse_select_stmt()?;
24062            if !matches!(self.peek(), Token::RParen) {
24063                return Err(self.err(alloc::format!(
24064                    "expected ')' after row comparison subquery, got {:?}",
24065                    self.peek()
24066                )));
24067            }
24068            self.advance();
24069            let Statement::Select(s) = inner else {
24070                unreachable!("parse_select_stmt always returns Statement::Select")
24071            };
24072            return Ok(Expr::RowCmpSubquery {
24073                row,
24074                op,
24075                subquery: Box::new(s),
24076            });
24077        }
24078        let mut rhs = alloc::vec![self.parse_expr(0)?];
24079        while matches!(self.peek(), Token::Comma) {
24080            self.advance();
24081            rhs.push(self.parse_expr(0)?);
24082        }
24083        if !matches!(self.peek(), Token::RParen) {
24084            return Err(self.err(alloc::format!(
24085                "expected ')' after right-hand row, got {:?}",
24086                self.peek()
24087            )));
24088        }
24089        self.advance();
24090        if rhs.len() != row.len() {
24091            // v7.39 (round 239) — PG's wording (42601).
24092            return Err(self.err("unequal number of entries in row expressions".to_string()));
24093        }
24094        Ok(match op {
24095            BinOp::Eq => row_eq(&row, &rhs),
24096            BinOp::NotEq => maybe_not(row_eq(&row, &rhs), true),
24097            BinOp::Lt => row_lex(&row, &rhs, BinOp::Lt, BinOp::Lt),
24098            BinOp::LtEq => row_lex(&row, &rhs, BinOp::Lt, BinOp::LtEq),
24099            BinOp::Gt => row_lex(&row, &rhs, BinOp::Gt, BinOp::Gt),
24100            BinOp::GtEq => row_lex(&row, &rhs, BinOp::Gt, BinOp::GtEq),
24101            _ => unreachable!("op restricted above"),
24102        })
24103    }
24104
24105    /// `LIKE p ESCAPE 'c'` — rewrite the pattern so the custom
24106    /// escape character becomes the matcher's default backslash:
24107    /// `c%` (escaped wildcard) → `\%`, `cc` (literal escape char)
24108    /// → the char itself, and any pre-existing backslash escapes
24109    /// itself so it stays literal. Both operands must be string
24110    /// literals — a runtime pattern would need matcher support.
24111    fn rewrite_like_escape(pattern: Expr, esc: Expr) -> Result<Expr, String> {
24112        let (Expr::Literal(Literal::String(p)), Expr::Literal(Literal::String(e))) =
24113            (&pattern, &esc)
24114        else {
24115            return Err(
24116                "LIKE ... ESCAPE requires string-literal pattern and escape \
24117                 (runtime escape characters are not supported yet)"
24118                    .into(),
24119            );
24120        };
24121        // v7.38 (read01 P6.18) — PG accepts `ESCAPE ''` to mean "no escape
24122        // character" (every `%`/`_` is a wildcard, nothing is escaped). Only a
24123        // multi-character escape is an error.
24124        let esc_ch: Option<char> = {
24125            let mut ch_iter = e.chars();
24126            match (ch_iter.next(), ch_iter.next()) {
24127                (Some(c), None) => Some(c),
24128                (None, _) => None,
24129                (Some(_), Some(_)) => {
24130                    return Err(alloc::format!(
24131                        "ESCAPE must be a single character, got {e:?}"
24132                    ));
24133                }
24134            }
24135        };
24136        let mut out = String::with_capacity(p.len() + 4);
24137        let mut chars = p.chars();
24138        while let Some(c) = chars.next() {
24139            if Some(c) == esc_ch {
24140                match chars.next() {
24141                    // Escaped wildcard / escaped escape → keep the
24142                    // next char literal via backslash.
24143                    Some(next) => {
24144                        out.push('\\');
24145                        out.push(next);
24146                    }
24147                    None => {
24148                        return Err("LIKE pattern ends with the escape character".into());
24149                    }
24150                }
24151            } else if c == '\\' && esc_ch != Some('\\') {
24152                // A raw backslash is literal under a custom (or absent) escape
24153                // — escape it for the backslash-based matcher.
24154                out.push('\\');
24155                out.push('\\');
24156            } else {
24157                out.push(c);
24158            }
24159        }
24160        Ok(Expr::Literal(Literal::String(out)))
24161    }
24162
24163    /// `x [NOT] LIKE ANY/ALL (ARRAY[p1, p2, …])` — quantified pattern
24164    /// match. Desugars to an OR (ANY) / AND (ALL) chain of per-element
24165    /// `x [NOT] LIKE pi`, which reproduces PG's three-valued semantics
24166    /// exactly (a NULL pattern makes an element NULL; `false OR NULL` =
24167    /// NULL, `true AND NULL` = NULL, …). ANY over an empty array is
24168    /// FALSE, ALL over empty is TRUE. Returns `None` when the token after
24169    /// LIKE is not `ANY(`/`ALL(`, so the caller falls back to a plain
24170    /// pattern. Only a literal `ARRAY[...]` is accepted today — a runtime
24171    /// array expression errors honestly rather than silently mismatching.
24172    fn try_like_any_all(
24173        &mut self,
24174        base: &Expr,
24175        negated: bool,
24176        case_insensitive: bool,
24177    ) -> Result<Option<Expr>, ParseError> {
24178        let is_any = match self.peek() {
24179            Token::All if matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) => false,
24180            Token::Ident(s)
24181                if s.eq_ignore_ascii_case("any")
24182                    && matches!(self.tokens.get(self.pos + 1), Some(Token::LParen)) =>
24183            {
24184                true
24185            }
24186            _ => return Ok(None),
24187        };
24188        self.advance(); // ANY / ALL
24189        self.advance(); // '('
24190        let arr = self.parse_expr(0)?;
24191        if !matches!(self.peek(), Token::RParen) {
24192            return Err(self.err(format!(
24193                "expected ')' after LIKE {} argument, got {:?}",
24194                if is_any { "ANY" } else { "ALL" },
24195                self.peek()
24196            )));
24197        }
24198        self.advance(); // ')'
24199        let Expr::Array(items) = arr else {
24200            return Err(self.err(
24201                "LIKE ANY/ALL currently requires a literal ARRAY[...] of patterns".to_string(),
24202            ));
24203        };
24204        let mut clauses = items.into_iter().map(|p| Expr::Like {
24205            expr: Box::new(base.clone()),
24206            pattern: Box::new(p),
24207            negated,
24208            case_insensitive,
24209        });
24210        let Some(first) = clauses.next() else {
24211            // ANY(empty) = FALSE, ALL(empty) = TRUE.
24212            return Ok(Some(Expr::Literal(Literal::Bool(!is_any))));
24213        };
24214        let op = if is_any { BinOp::Or } else { BinOp::And };
24215        let combined = clauses.fold(first, |acc, c| Expr::Binary {
24216            lhs: Box::new(acc),
24217            op,
24218            rhs: Box::new(c),
24219        });
24220        Ok(Some(combined))
24221    }
24222
24223    /// `x BETWEEN low AND high`  →  `(x >= low) AND (x <= high)`, wrapped in
24224    /// `NOT` when `negated`. Bounds parse at precedence 5 so the trailing
24225    /// `AND` is not swallowed.
24226    fn parse_between_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
24227        self.advance(); // BETWEEN
24228        // SYMMETRIC — the bounds may arrive in either order; both
24229        // orientations OR together. ASYMMETRIC is the default and
24230        // absorbs as noise.
24231        let symmetric = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("symmetric"))
24232        {
24233            self.advance();
24234            true
24235        } else {
24236            if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("asymmetric")) {
24237                self.advance();
24238            }
24239            false
24240        };
24241        let low = self.parse_expr(6)?;
24242        if !matches!(self.peek(), Token::And) {
24243            return Err(self.err(format!(
24244                "expected AND after BETWEEN low bound, got {:?}",
24245                self.peek()
24246            )));
24247        }
24248        self.advance();
24249        let high = self.parse_expr(6)?;
24250        let target = Box::new(expr);
24251        let range = |lo: Expr, hi: Expr| Expr::Binary {
24252            lhs: Box::new(Expr::Binary {
24253                lhs: target.clone(),
24254                op: BinOp::GtEq,
24255                rhs: Box::new(lo),
24256            }),
24257            op: BinOp::And,
24258            rhs: Box::new(Expr::Binary {
24259                lhs: target.clone(),
24260                op: BinOp::LtEq,
24261                rhs: Box::new(hi),
24262            }),
24263        };
24264        let combined = if symmetric {
24265            Expr::Binary {
24266                lhs: Box::new(range(low.clone(), high.clone())),
24267                op: BinOp::Or,
24268                rhs: Box::new(range(high, low)),
24269            }
24270        } else {
24271            range(low, high)
24272        };
24273        Ok(maybe_not(combined, negated))
24274    }
24275
24276    /// `x IN (a, b, c)`  →  chained OR of equalities. Empty list collapses
24277    /// to FALSE (TRUE under NOT IN), matching standard SQL semantics.
24278    /// v4.11: parse `WITH name AS (SELECT ...) [, ...] SELECT ...`.
24279    /// Caller already consumed the leading `WITH` ident.
24280    /// v7.38 (read01) — recursive-CTE well-formedness. PG rejects ORDER BY
24281    /// / LIMIT / OFFSET anywhere in a recursive query, and a recursive
24282    /// self-reference that appears more than once in a single term.
24283    fn validate_recursive_cte(&self, cte: &crate::ast::Cte) -> Result<(), ParseError> {
24284        use crate::ast::{CteBody, SelectStatement};
24285        if !cte.recursive {
24286            return Ok(());
24287        }
24288        let CteBody::Select(body) = &cte.body else {
24289            return Ok(());
24290        };
24291        // A recursive CTE body is `base UNION [ALL] recursive [UNION …]`;
24292        // check the anchor and every peer term.
24293        let has_order = |s: &SelectStatement| !s.order_by.is_empty();
24294        let has_limit = |s: &SelectStatement| s.limit.is_some() || s.offset.is_some();
24295        if has_order(body) || body.unions.iter().any(|(_, u)| has_order(u)) {
24296            return Err(self.err(String::from(
24297                "ORDER BY in a recursive query is not implemented",
24298            )));
24299        }
24300        if has_limit(body) || body.unions.iter().any(|(_, u)| has_limit(u)) {
24301            return Err(self.err(String::from(
24302                "LIMIT in a recursive query is not implemented",
24303            )));
24304        }
24305        let self_refs = |s: &SelectStatement| -> usize {
24306            let Some(from) = &s.from else {
24307                return 0;
24308            };
24309            let mut n = usize::from(from.primary.name.eq_ignore_ascii_case(&cte.name));
24310            for j in &from.joins {
24311                if j.table.name.eq_ignore_ascii_case(&cte.name) {
24312                    n += 1;
24313                }
24314            }
24315            n
24316        };
24317        if body.unions.iter().any(|(_, u)| self_refs(u) > 1) {
24318            return Err(self.err(alloc::format!(
24319                "recursive reference to query \"{}\" must not appear more than once",
24320                cte.name
24321            )));
24322        }
24323        // v7.39 (round 145, parse_cte.c) — the remaining well-formedness rules
24324        // apply only when the body actually references itself (a non-self-
24325        // referencing CTE under WITH RECURSIVE may use any set-op shape).
24326        let anchor_refs = self_refs(body);
24327        let union_refs = body.unions.iter().any(|(_, u)| self_refs(u) > 0);
24328        if anchor_refs > 0 || union_refs {
24329            // Shape: the top level must be UNION [ALL] arms only. A self-ref
24330            // under INTERSECT / EXCEPT (or with no set-op at all) is PG's
24331            // "does not have the form" error — SPG used to compute a value.
24332            if body.unions.is_empty()
24333                || body.unions.iter().any(|(k, _)| {
24334                    !matches!(
24335                        k,
24336                        crate::ast::UnionKind::Distinct | crate::ast::UnionKind::All
24337                    )
24338                })
24339            {
24340                return Err(self.err(alloc::format!(
24341                    "recursive query \"{}\" does not have the form non-recursive-term \
24342                     UNION [ALL] recursive-term",
24343                    cte.name
24344                )));
24345            }
24346            if anchor_refs > 0 {
24347                return Err(self.err(alloc::format!(
24348                    "recursive reference to query \"{}\" must not appear within its non-recursive term",
24349                    cte.name
24350                )));
24351            }
24352        }
24353        let is_self = |t: &crate::ast::TableRef| t.name.eq_ignore_ascii_case(&cte.name);
24354        for (_, u) in &body.unions {
24355            if self_refs(u) == 0 {
24356                continue;
24357            }
24358            // The self-reference must not sit on the nullable side of an outer
24359            // join (LEFT: right side; RIGHT: everything before it; FULL: both).
24360            if let Some(from) = &u.from {
24361                for (i, j) in from.joins.iter().enumerate() {
24362                    let left_has_self = is_self(&from.primary)
24363                        || from.joins[..i].iter().any(|pj| is_self(&pj.table));
24364                    let violated = match j.kind {
24365                        crate::ast::JoinKind::Left => is_self(&j.table),
24366                        crate::ast::JoinKind::Right => left_has_self,
24367                        crate::ast::JoinKind::FullOuter => is_self(&j.table) || left_has_self,
24368                        _ => false,
24369                    };
24370                    if violated {
24371                        return Err(self.err(alloc::format!(
24372                            "recursive reference to query \"{}\" must not appear within an outer join",
24373                            cte.name
24374                        )));
24375                    }
24376                }
24377            }
24378            // No aggregates at the top level of the recursive term (SPG used
24379            // to run them and surface a misleading downstream error).
24380            let mut items_and_having: Vec<&Expr> = Vec::new();
24381            for it in &u.items {
24382                if let crate::ast::SelectItem::Expr { expr, .. } = it {
24383                    items_and_having.push(expr);
24384                }
24385            }
24386            if let Some(h) = &u.having {
24387                items_and_having.push(h);
24388            }
24389            for e in items_and_having {
24390                if expr_has_toplevel_aggregate(e) {
24391                    return Err(self.err(String::from(
24392                        "aggregate functions are not allowed in a recursive query's recursive term",
24393                    )));
24394                }
24395            }
24396        }
24397        // A self-reference inside a sublink expression (EXISTS / IN / scalar
24398        // subquery) anywhere in the body is rejected; a plain FROM derived
24399        // table is legal in PG and untouched here.
24400        let mut all_terms: Vec<&SelectStatement> = alloc::vec![body];
24401        all_terms.extend(body.unions.iter().map(|(_, u)| u));
24402        for term in all_terms {
24403            if select_has_self_ref_in_sublink(term, &cte.name) {
24404                return Err(self.err(alloc::format!(
24405                    "recursive reference to query \"{}\" must not appear within a subquery",
24406                    cte.name
24407                )));
24408            }
24409        }
24410        Ok(())
24411    }
24412
24413    /// v7.38 (read01 U16) — desugar a CTE's SEARCH / CYCLE clause into
24414    /// extra body columns, mirroring PG's `rewriteSearchAndCycle`. Runs
24415    /// right after parse so the engine sees a plain recursive CTE with the
24416    /// tracking columns already projected. DEPTH FIRST and CYCLE are
24417    /// supported; BREADTH FIRST needs numeric-composite ordering SPG's
24418    /// text-rendered rows can't provide, and errors honestly.
24419    fn desugar_cte_search_cycle(&self, cte: &mut crate::ast::Cte) -> Result<(), ParseError> {
24420        use crate::ast::{BinOp, ColumnName, CteBody, Expr, Literal, SelectItem, UnOp};
24421        if cte.search.is_none() && cte.cycle.is_none() {
24422            return Ok(());
24423        }
24424        let cte_name = cte.name.clone();
24425        let col_names = cte.column_overrides.clone();
24426        if col_names.is_empty() {
24427            return Err(
24428                self.err("SEARCH / CYCLE requires an explicit WITH name(cols) column list".into())
24429            );
24430        }
24431        let search = cte.search.take();
24432        let cycle = cte.cycle.take();
24433        let mut extra_cols: Vec<String> = Vec::new();
24434        let col_ref = |name: &str| {
24435            Expr::Column(ColumnName {
24436                qualifier: Some(cte_name.clone()),
24437                name: name.to_string(),
24438            })
24439        };
24440        // Position of a SEARCH/CYCLE column within the CTE's column list.
24441        let pos_of = |name: &str| -> Result<usize, ParseError> {
24442            col_names
24443                .iter()
24444                .position(|c| c.eq_ignore_ascii_case(name))
24445                .ok_or_else(|| {
24446                    self.err(format!("SEARCH/CYCLE column {name:?} is not a CTE column"))
24447                })
24448        };
24449        let row_of = |items: &[SelectItem], positions: &[usize]| -> Result<Expr, ParseError> {
24450            let mut args = Vec::with_capacity(positions.len());
24451            for &p in positions {
24452                match items.get(p) {
24453                    Some(SelectItem::Expr { expr, .. }) => args.push(expr.clone()),
24454                    _ => {
24455                        return Err(self.err(
24456                            "SEARCH/CYCLE column maps to a non-expression select item".into(),
24457                        ));
24458                    }
24459                }
24460            }
24461            Ok(Expr::FunctionCall {
24462                name: "row".into(),
24463                args,
24464            })
24465        };
24466        let CteBody::Select(body) = &mut cte.body else {
24467            return Err(self.err("SEARCH / CYCLE requires a SELECT CTE body".into()));
24468        };
24469        if body.unions.is_empty() {
24470            return Err(self.err("SEARCH / CYCLE requires a recursive (UNION) CTE".into()));
24471        }
24472        let rec = body.unions.len() - 1; // recursive term = last UNION peer
24473
24474        if let Some(srch) = search {
24475            // v7.38 (T31) — SEARCH's SET column is ORDER BY'd, and PG's key is a
24476            // `record[]` (DEPTH) or `(depth, keys…)` record (BREADTH). SPG has
24477            // no typed `record[]`, but element-wise array ORDER BY is correct
24478            // (`[1,2] < [1,10] < [2]`), so a SINGLE scalar BY column maps
24479            // exactly onto a typed array: DEPTH is the root→node path
24480            // `array_append(parent, key)`, BREADTH is `[depth, key]`. This
24481            // orders numerically (multi-digit keys included), matching PG.
24482            //
24483            // A multi-column BY would need a record[] to keep the per-node key
24484            // tuple orderable, which SPG can't express — error honestly there
24485            // rather than mis-order.
24486            if srch.by_columns.len() != 1 {
24487                return Err(self.err(
24488                    "SEARCH … BY with multiple columns needs typed record[] ordering \
24489                     SPG doesn't have yet; a single BY column is supported"
24490                        .into(),
24491                ));
24492            }
24493            let key_pos = pos_of(&srch.by_columns[0])?;
24494            let base_key = match body.items.get(key_pos) {
24495                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24496                _ => {
24497                    return Err(
24498                        self.err("SEARCH BY column maps to a non-expression select item".into())
24499                    );
24500                }
24501            };
24502            let rec_key = match body.unions[rec].1.items.get(key_pos) {
24503                Some(SelectItem::Expr { expr, .. }) => expr.clone(),
24504                _ => {
24505                    return Err(
24506                        self.err("SEARCH BY column maps to a non-expression select item".into())
24507                    );
24508                }
24509            };
24510            if srch.depth_first {
24511                // base: ARRAY[key]; rec: array_append(cte.set, key).
24512                body.items.push(SelectItem::Expr {
24513                    expr: Expr::Array(alloc::vec![base_key]),
24514                    alias: Some(srch.set_column.clone()),
24515                });
24516                body.unions[rec].1.items.push(SelectItem::Expr {
24517                    expr: Expr::FunctionCall {
24518                        name: "array_append".into(),
24519                        args: alloc::vec![col_ref(&srch.set_column), rec_key],
24520                    },
24521                    alias: Some(srch.set_column.clone()),
24522                });
24523            } else {
24524                // BREADTH: [depth, key]; depth starts at 0 and increments. The
24525                // leading depth element dominates the element-wise comparison,
24526                // so shallower rows sort first, then by key — PG's (depth, key).
24527                body.items.push(SelectItem::Expr {
24528                    expr: Expr::Array(alloc::vec![Expr::Literal(Literal::Integer(0)), base_key,]),
24529                    alias: Some(srch.set_column.clone()),
24530                });
24531                // rec depth = cte.set[1] + 1.
24532                let parent_depth = Expr::ArraySubscript {
24533                    target: Box::new(col_ref(&srch.set_column)),
24534                    index: Box::new(Expr::Literal(Literal::Integer(1))),
24535                };
24536                body.unions[rec].1.items.push(SelectItem::Expr {
24537                    expr: Expr::Array(alloc::vec![
24538                        Expr::Binary {
24539                            lhs: Box::new(parent_depth),
24540                            op: BinOp::Add,
24541                            rhs: Box::new(Expr::Literal(Literal::Integer(1))),
24542                        },
24543                        rec_key,
24544                    ]),
24545                    alias: Some(srch.set_column.clone()),
24546                });
24547            }
24548            extra_cols.push(srch.set_column);
24549        }
24550
24551        if let Some(cyc) = cycle {
24552            let positions: Vec<usize> = cyc
24553                .columns
24554                .iter()
24555                .map(|c| pos_of(c))
24556                .collect::<Result<_, _>>()?;
24557            // v7.38 (read01, T9) — ROW(cols) is now a first-class composite, so
24558            // cast it to text for the cycle path: membership only needs equality,
24559            // and the record text form gives SPG a TextArray path (SPG has no
24560            // typed record[] array). Cycle detection is unaffected.
24561            let base_row = Expr::Cast {
24562                expr: Box::new(row_of(&body.items, &positions)?),
24563                target: CastTarget::Text,
24564            };
24565            let rec_row = Expr::Cast {
24566                expr: Box::new(row_of(&body.unions[rec].1.items, &positions)?),
24567                target: CastTarget::Text,
24568            };
24569            let mark = cyc.mark_value.clone().unwrap_or(Literal::Bool(true));
24570            let dflt = cyc.default_value.clone().unwrap_or(Literal::Bool(false));
24571            // base: <default> AS mark, ARRAY[ROW(cols)] AS path.
24572            body.items.push(SelectItem::Expr {
24573                expr: Expr::Literal(dflt.clone()),
24574                alias: Some(cyc.mark_column.clone()),
24575            });
24576            body.items.push(SelectItem::Expr {
24577                expr: Expr::Array(alloc::vec![base_row]),
24578                alias: Some(cyc.path_column.clone()),
24579            });
24580            // rec mark: ROW(cols) already in the path → cycle.
24581            let hit = Expr::AnyAll {
24582                expr: Box::new(rec_row.clone()),
24583                op: BinOp::Eq,
24584                array: Box::new(col_ref(&cyc.path_column)),
24585                is_any: true,
24586            };
24587            let mark_expr = if cyc.mark_value.is_some() || cyc.default_value.is_some() {
24588                Expr::Case {
24589                    operand: None,
24590                    branches: alloc::vec![(hit, Expr::Literal(mark))],
24591                    else_branch: Some(Box::new(Expr::Literal(dflt))),
24592                }
24593            } else {
24594                hit
24595            };
24596            body.unions[rec].1.items.push(SelectItem::Expr {
24597                expr: mark_expr,
24598                alias: Some(cyc.mark_column.clone()),
24599            });
24600            // rec path: array_append(cte.path, ROW(cols)).
24601            body.unions[rec].1.items.push(SelectItem::Expr {
24602                expr: Expr::FunctionCall {
24603                    name: "array_append".into(),
24604                    args: alloc::vec![col_ref(&cyc.path_column), rec_row],
24605                },
24606                alias: Some(cyc.path_column.clone()),
24607            });
24608            // rec WHERE: AND NOT cte.mark — stop expanding a cycled row.
24609            let stop = Expr::Unary {
24610                op: UnOp::Not,
24611                expr: Box::new(col_ref(&cyc.mark_column)),
24612            };
24613            let w = &mut body.unions[rec].1.where_;
24614            *w = Some(match w.take() {
24615                Some(prev) => Expr::Binary {
24616                    lhs: Box::new(prev),
24617                    op: BinOp::And,
24618                    rhs: Box::new(stop),
24619                },
24620                None => stop,
24621            });
24622            extra_cols.push(cyc.mark_column);
24623            extra_cols.push(cyc.path_column);
24624        }
24625        cte.column_overrides.extend(extra_cols);
24626        Ok(())
24627    }
24628
24629    /// v7.38 (read01 U16) — `SEARCH { DEPTH | BREADTH } FIRST BY col [,
24630    /// col…] SET seqcol`. Returns None when the next token isn't SEARCH.
24631    fn parse_cte_search_clause(&mut self) -> Result<Option<crate::ast::SearchClause>, ParseError> {
24632        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("search")) {
24633            return Ok(None);
24634        }
24635        self.advance(); // SEARCH
24636        let depth_first = match self.peek() {
24637            Token::Ident(s) if s.eq_ignore_ascii_case("depth") => true,
24638            Token::Ident(s) if s.eq_ignore_ascii_case("breadth") => false,
24639            other => {
24640                return Err(self.err(format!(
24641                    "expected DEPTH or BREADTH after SEARCH, got {other:?}"
24642                )));
24643            }
24644        };
24645        self.advance();
24646        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("first")) {
24647            return Err(self.err(format!(
24648                "expected FIRST after SEARCH mode, got {:?}",
24649                self.peek()
24650            )));
24651        }
24652        self.advance();
24653        if !self.peek_is_by() {
24654            return Err(self.err(format!(
24655                "expected BY after SEARCH … FIRST, got {:?}",
24656                self.peek()
24657            )));
24658        }
24659        self.advance();
24660        let mut by_columns = alloc::vec![self.expect_ident_like()?];
24661        while matches!(self.peek(), Token::Comma) {
24662            self.advance();
24663            by_columns.push(self.expect_ident_like()?);
24664        }
24665        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24666            return Err(self.err(format!(
24667                "expected SET in SEARCH clause, got {:?}",
24668                self.peek()
24669            )));
24670        }
24671        self.advance();
24672        let set_column = self.expect_ident_like()?;
24673        Ok(Some(crate::ast::SearchClause {
24674            depth_first,
24675            by_columns,
24676            set_column,
24677        }))
24678    }
24679
24680    /// v7.38 (read01 U16) — `CYCLE col [, col…] SET markcol [TO v DEFAULT w]
24681    /// USING pathcol`. Returns None when the next token isn't CYCLE.
24682    fn parse_cte_cycle_clause(&mut self) -> Result<Option<crate::ast::CycleClause>, ParseError> {
24683        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("cycle")) {
24684            return Ok(None);
24685        }
24686        self.advance(); // CYCLE
24687        let mut columns = alloc::vec![self.expect_ident_like()?];
24688        while matches!(self.peek(), Token::Comma) {
24689            self.advance();
24690            columns.push(self.expect_ident_like()?);
24691        }
24692        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("set")) {
24693            return Err(self.err(format!(
24694                "expected SET in CYCLE clause, got {:?}",
24695                self.peek()
24696            )));
24697        }
24698        self.advance();
24699        let mark_column = self.expect_ident_like()?;
24700        let mut mark_value = None;
24701        let mut default_value = None;
24702        if matches!(self.peek(), Token::To) {
24703            self.advance();
24704            mark_value = Some(self.parse_cycle_literal()?);
24705            if !matches!(self.peek(), Token::Default) {
24706                return Err(self.err(format!(
24707                    "expected DEFAULT after CYCLE … TO, got {:?}",
24708                    self.peek()
24709                )));
24710            }
24711            self.advance();
24712            default_value = Some(self.parse_cycle_literal()?);
24713        }
24714        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
24715            return Err(self.err(format!(
24716                "expected USING in CYCLE clause, got {:?}",
24717                self.peek()
24718            )));
24719        }
24720        self.advance();
24721        let path_column = self.expect_ident_like()?;
24722        Ok(Some(crate::ast::CycleClause {
24723            columns,
24724            mark_column,
24725            mark_value,
24726            default_value,
24727            path_column,
24728        }))
24729    }
24730
24731    /// The mark / default value in a CYCLE `TO v DEFAULT w` — a bare
24732    /// literal (string / bool / number) in PG.
24733    fn parse_cycle_literal(&mut self) -> Result<crate::ast::Literal, ParseError> {
24734        match self.parse_expr(0)? {
24735            Expr::Literal(l) => Ok(l),
24736            other => Err(self.err(format!(
24737                "CYCLE mark/default value must be a literal, got {other:?}"
24738            ))),
24739        }
24740    }
24741
24742    fn parse_with_cte_then_select(&mut self) -> Result<Statement, ParseError> {
24743        // v4.22: WITH RECURSIVE — optional keyword right after WITH.
24744        // Comes through as an identifier; consume it if present and
24745        // mark every CTE in the clause as recursive (PG semantics —
24746        // the flag is per-WITH, not per-CTE).
24747        let mut recursive = false;
24748        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
24749            && s.eq_ignore_ascii_case("recursive")
24750        {
24751            self.advance();
24752            recursive = true;
24753        }
24754        let mut ctes = Vec::new();
24755        loop {
24756            let name = self.expect_ident_like()?;
24757            // v4.22: optional column-name list — `WITH t(a,b,c) AS ...`.
24758            // PG uses these to rename the body's output columns; we
24759            // do the same below by overriding `columns[i].name`.
24760            let column_overrides: Vec<String> = if matches!(self.peek(), Token::LParen) {
24761                self.advance();
24762                let mut names = Vec::new();
24763                loop {
24764                    names.push(self.expect_ident_like()?);
24765                    if matches!(self.peek(), Token::Comma) {
24766                        self.advance();
24767                        continue;
24768                    }
24769                    break;
24770                }
24771                if !matches!(self.peek(), Token::RParen) {
24772                    return Err(self.err(format!(
24773                        "expected ')' to close CTE column list, got {:?}",
24774                        self.peek()
24775                    )));
24776                }
24777                self.advance();
24778                names
24779            } else {
24780                Vec::new()
24781            };
24782            // AS is a reserved Token::As (used by SELECT-item / FROM
24783            // aliasing) — handle it specially rather than as a bare
24784            // ident.
24785            if !matches!(self.peek(), Token::As) {
24786                return Err(self.err(format!(
24787                    "expected AS after CTE name {name:?}, got {:?}",
24788                    self.peek()
24789                )));
24790            }
24791            self.advance();
24792            // v7.37.17 (17.6 siblings) — PG 12+ `AS [NOT]
24793            // MATERIALIZED` optimizer hints. SPG materialises every
24794            // CTE, so both spellings are accepted and absorbed.
24795            if matches!(self.peek(), Token::Not) {
24796                self.advance(); // NOT
24797                if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24798                    if s.eq_ignore_ascii_case("materialized"))
24799                {
24800                    self.advance();
24801                } else {
24802                    return Err(self.err(format!(
24803                        "expected MATERIALIZED after AS NOT, got {:?}",
24804                        self.peek()
24805                    )));
24806                }
24807            } else if matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s)
24808                if s.eq_ignore_ascii_case("materialized"))
24809            {
24810                self.advance();
24811            }
24812            if !matches!(self.peek(), Token::LParen) {
24813                return Err(self.err(format!(
24814                    "expected '(' after AS in WITH clause, got {:?}",
24815                    self.peek()
24816                )));
24817            }
24818            self.advance();
24819            // v7.37.43-T4.4 — accept INSERT / UPDATE / DELETE (with
24820            // RETURNING) as the CTE body in addition to SELECT.
24821            // PG writable CTE semantics. UPDATE / DELETE come in as
24822            // bare Idents (lexer keeps SELECT / INSERT as reserved
24823            // tokens but treats the rest of DML as case-insensitive
24824            // idents).
24825            let is_update_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24826            let is_delete_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24827            let is_merge_kw = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24828            let body = match self.peek() {
24829                Token::Select => {
24830                    let inner = self.parse_select_stmt()?;
24831                    let Statement::Select(s) = inner else {
24832                        unreachable!("parse_select_stmt returns Select");
24833                    };
24834                    crate::ast::CteBody::Select(s)
24835                }
24836                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24837                // `SELECT * FROM t` this way and accepts it wherever a
24838                // SELECT goes, so the CTE body dispatch needs its own
24839                // arm: this match is keyed on the FIRST token, and
24840                // `Token::Table` fell through to a tail that then
24841                // rejected what it got. `parse_table_shorthand` has
24842                // returned a desugared SelectStatement since the
24843                // shorthand landed — only the routing was missing.
24844                // Round 868 found this by putting the shorthand in a
24845                // subquery; every earlier check used a top-level form.
24846                // v7.39 (round 869) — `WITH x AS (TABLE t)`. PG spells
24847                // `SELECT * FROM t` this way and accepts it wherever a
24848                // SELECT goes, so the CTE body dispatch needs its own
24849                // arm: this match is keyed on the FIRST token, and
24850                // `Token::Table` fell through to a tail that rejected
24851                // what it got. `parse_table_shorthand` has returned a
24852                // desugared SelectStatement since the shorthand landed —
24853                // only the routing was missing, here and in the derived
24854                // table's second-token gate. Round 868 found both by
24855                // putting the shorthand in a subquery; every earlier
24856                // check had used a top-level form.
24857                Token::Table
24858                    if matches!(
24859                        self.tokens.get(self.pos + 1),
24860                        Some(Token::Ident(_) | Token::QuotedIdent(_))
24861                    ) =>
24862                {
24863                    let mut head = self.parse_table_shorthand()?;
24864                    self.parse_setop_chain_into(&mut head)?;
24865                    self.parse_select_tail_into(&mut head)?;
24866                    crate::ast::CteBody::Select(head)
24867                }
24868                // v7.37.17 (17.6 siblings) — VALUES as a CTE body:
24869                // WITH t(a) AS (VALUES (1), (2)) … lowers through
24870                // the shared rows helper onto a Select body.
24871                Token::Values => {
24872                    self.advance(); // VALUES
24873                    let mut head = self.parse_values_rows_body()?;
24874                    // A VALUES seed can head a set-operation chain —
24875                    // WITH RECURSIVE t(n) AS (VALUES(1) UNION ALL
24876                    // SELECT n+1 FROM t …). Attach any trailing
24877                    // UNION / INTERSECT / EXCEPT peers so the
24878                    // recursive-CTE body parses like the SELECT seed.
24879                    self.parse_setop_chain_into(&mut head)?;
24880                    crate::ast::CteBody::Select(head)
24881                }
24882                Token::Insert => {
24883                    let inner = self.parse_one_statement()?;
24884                    let Statement::Insert(s) = inner else {
24885                        unreachable!("Token::Insert routes to Insert");
24886                    };
24887                    crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24888                }
24889                _ if is_update_kw => {
24890                    let inner = self.parse_one_statement()?;
24891                    let Statement::Update(s) = inner else {
24892                        return Err(
24893                            self.err(format!("expected UPDATE inside WITH (…), got {inner:?}"))
24894                        );
24895                    };
24896                    crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24897                }
24898                _ if is_delete_kw => {
24899                    let inner = self.parse_one_statement()?;
24900                    let Statement::Delete(s) = inner else {
24901                        return Err(
24902                            self.err(format!("expected DELETE inside WITH (…), got {inner:?}"))
24903                        );
24904                    };
24905                    crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24906                }
24907                // v7.39 (round 149) — PG 17 allows MERGE as a
24908                // data-modifying CTE body.
24909                _ if is_merge_kw => {
24910                    let inner = self.parse_one_statement()?;
24911                    let Statement::Merge(s) = inner else {
24912                        return Err(
24913                            self.err(format!("expected MERGE inside WITH (…), got {inner:?}"))
24914                        );
24915                    };
24916                    crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24917                }
24918                // v7.39 (round 151) — a CTE body may itself be
24919                // WITH-headed (PG grammar: PreparableStmt carries its
24920                // own with_clause). The nested statement keeps its own
24921                // ctes; the modifying-CTE-at-top-level rule is enforced
24922                // at execution.
24923                Token::Ident(s) if s.eq_ignore_ascii_case("with") => {
24924                    self.advance(); // WITH
24925                    match self.parse_with_cte_then_select()? {
24926                        Statement::Select(s) => crate::ast::CteBody::Select(s),
24927                        Statement::Insert(s) => {
24928                            crate::ast::CteBody::Insert(alloc::boxed::Box::new(s))
24929                        }
24930                        Statement::Update(s) => {
24931                            crate::ast::CteBody::Update(alloc::boxed::Box::new(s))
24932                        }
24933                        Statement::Delete(s) => {
24934                            crate::ast::CteBody::Delete(alloc::boxed::Box::new(s))
24935                        }
24936                        Statement::Merge(s) => {
24937                            crate::ast::CteBody::Merge(alloc::boxed::Box::new(s))
24938                        }
24939
24940                        other => {
24941                            return Err(self.err(format!(
24942                                "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24943                            )));
24944                        }
24945                    }
24946                }
24947                other => {
24948                    return Err(self.err(format!(
24949                        "WITH body must be SELECT / INSERT / UPDATE / DELETE / MERGE, got {other:?}"
24950                    )));
24951                }
24952            };
24953            if !matches!(self.peek(), Token::RParen) {
24954                return Err(self.err(format!(
24955                    "expected ')' after CTE body, got {:?}",
24956                    self.peek()
24957                )));
24958            }
24959            self.advance();
24960            // v7.38 (read01 U16) — optional SEARCH / CYCLE on a recursive
24961            // CTE, desugared into extra body columns by the engine.
24962            let search = self.parse_cte_search_clause()?;
24963            let cycle = self.parse_cte_cycle_clause()?;
24964            let mut cte = crate::ast::Cte {
24965                name,
24966                body,
24967                recursive,
24968                column_overrides,
24969                search,
24970                cycle,
24971            };
24972            self.validate_recursive_cte(&cte)?;
24973            self.desugar_cte_search_cycle(&mut cte)?;
24974            ctes.push(cte);
24975            if matches!(self.peek(), Token::Comma) {
24976                self.advance();
24977                continue;
24978            }
24979            break;
24980        }
24981        // v7.37.43-T4.4 — the outer body may be SELECT (classical),
24982        // or INSERT / UPDATE / DELETE (writable CTE outer). Attach
24983        // the parsed CTEs to whichever statement the body produces.
24984        let outer_is_update = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("update"));
24985        let outer_is_delete = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("delete"));
24986        let outer_is_merge = matches!(self.peek(), Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("merge"));
24987        match self.peek() {
24988            Token::Select => {
24989                let body_stmt = self.parse_select_stmt()?;
24990                let Statement::Select(mut body) = body_stmt else {
24991                    unreachable!()
24992                };
24993                body.ctes = ctes;
24994                Ok(Statement::Select(body))
24995            }
24996            Token::Insert => {
24997                let body_stmt = self.parse_one_statement()?;
24998                let Statement::Insert(mut body) = body_stmt else {
24999                    unreachable!()
25000                };
25001                body.ctes = ctes;
25002                Ok(Statement::Insert(body))
25003            }
25004            _ if outer_is_update => {
25005                let body_stmt = self.parse_one_statement()?;
25006                let Statement::Update(mut body) = body_stmt else {
25007                    return Err(self.err(format!("expected UPDATE after WITH clause")));
25008                };
25009                body.ctes = ctes;
25010                Ok(Statement::Update(body))
25011            }
25012            _ if outer_is_delete => {
25013                let body_stmt = self.parse_one_statement()?;
25014                let Statement::Delete(mut body) = body_stmt else {
25015                    return Err(self.err(format!("expected DELETE after WITH clause")));
25016                };
25017                body.ctes = ctes;
25018                Ok(Statement::Delete(body))
25019            }
25020            // v7.39 (round 149) — PG 15 allows a WITH clause on MERGE;
25021            // WITH RECURSIVE is rejected with PG's exact message
25022            // (parse analysis, transformWithClause).
25023            _ if outer_is_merge => {
25024                if recursive {
25025                    return Err(self.err(String::from(
25026                        "WITH RECURSIVE is not supported for MERGE statement",
25027                    )));
25028                }
25029                let body_stmt = self.parse_one_statement()?;
25030                let Statement::Merge(mut body) = body_stmt else {
25031                    return Err(self.err(format!("expected MERGE after WITH clause")));
25032                };
25033                body.ctes = ctes;
25034                Ok(Statement::Merge(body))
25035            }
25036            other => Err(self.err(format!(
25037                "expected SELECT / INSERT / UPDATE / DELETE / MERGE after WITH clause, got {other:?}"
25038            ))),
25039        }
25040    }
25041
25042    /// v4.10: parse `EXISTS (SELECT ...)`. Caller (`parse_atom`)
25043    /// already consumed the leading `EXISTS` ident via
25044    /// `self.advance()`.
25045    /// v7.13.0 — parse the rest of a `CASE … END` expression after
25046    /// the leading `CASE` ident has been consumed (mailrs round-5
25047    /// G9). Supports both the searched form
25048    /// (`CASE WHEN cond THEN val …`) and the simple form
25049    /// (`CASE operand WHEN val THEN val …`).
25050    fn parse_case_atom(&mut self) -> Result<Expr, ParseError> {
25051        // Disambiguate searched vs simple form: if the next token
25052        // is `WHEN`, we're in the searched form. Otherwise the
25053        // intervening expression is the operand.
25054        let operand = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("when")) {
25055            None
25056        } else {
25057            Some(Box::new(self.parse_expr(0)?))
25058        };
25059        let mut branches: Vec<(Expr, Expr)> = Vec::new();
25060        loop {
25061            match self.peek() {
25062                Token::Ident(s) if s.eq_ignore_ascii_case("when") => {
25063                    self.advance();
25064                    let cond = self.parse_expr(0)?;
25065                    match self.peek() {
25066                        Token::Ident(t) if t.eq_ignore_ascii_case("then") => {
25067                            self.advance();
25068                        }
25069                        other => {
25070                            return Err(self.err(alloc::format!(
25071                                "expected THEN after CASE WHEN <expr>, got {other:?}"
25072                            )));
25073                        }
25074                    }
25075                    let value = self.parse_expr(0)?;
25076                    branches.push((cond, value));
25077                }
25078                _ => break,
25079            }
25080        }
25081        if branches.is_empty() {
25082            return Err(self.err("CASE requires at least one WHEN … THEN … branch".into()));
25083        }
25084        let else_branch = if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("else"))
25085        {
25086            self.advance();
25087            Some(Box::new(self.parse_expr(0)?))
25088        } else {
25089            None
25090        };
25091        match self.peek() {
25092            Token::Ident(s) if s.eq_ignore_ascii_case("end") => {
25093                self.advance();
25094            }
25095            other => {
25096                return Err(self.err(alloc::format!(
25097                    "expected END to close CASE expression, got {other:?}"
25098                )));
25099            }
25100        }
25101        Ok(Expr::Case {
25102            operand,
25103            branches,
25104            else_branch,
25105        })
25106    }
25107
25108    /// v7.39 (round 151) — nested `WITH … SELECT …` in a subquery /
25109    /// query-source position (EXISTS / IN / INSERT source / CTE body /
25110    /// view body). Caller consumed the WITH keyword. Only a SELECT
25111    /// outer is grammatical here; the data-modifying-CTE-at-top-level
25112    /// rule (PG 0A000) is enforced at execution, where the SQLSTATE
25113    /// maps correctly.
25114    fn parse_nested_with_select(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25115        let inner = self.parse_with_cte_then_select()?;
25116        match inner {
25117            Statement::Select(s) => Ok(s),
25118            other => Err(self.err(format!(
25119                "expected SELECT after WITH in a subquery, got {other:?}"
25120            ))),
25121        }
25122    }
25123
25124    /// True when the next token is the (unquoted) WITH keyword. WITH is
25125    /// reserved in PG, so a bare `with` can never be a column reference
25126    /// in these positions; a quoted `"with"` stays an identifier.
25127    fn peek_is_with_kw(&self) -> bool {
25128        matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("with"))
25129    }
25130
25131    /// v7.39 (round 153) — the `ANY / ALL ( [WITH …] SELECT … )` body.
25132    /// `#[inline(never)]` keeps the large SelectStatement temporaries
25133    /// off parse_expr's recursive frame (the nesting-budget stack
25134    /// cliff — see the round-153 gate regression).
25135    #[inline(never)]
25136    fn parse_any_all_select_body(&mut self) -> Result<crate::ast::SelectStatement, ParseError> {
25137        if self.peek_is_with_kw() {
25138            self.advance();
25139            self.parse_nested_with_select()
25140        } else {
25141            match self.parse_select_stmt()? {
25142                Statement::Select(s) => Ok(s),
25143                other => Err(self.err(alloc::format!(
25144                    "expected SELECT inside ANY/ALL, got {other:?}"
25145                ))),
25146            }
25147        }
25148    }
25149
25150    fn parse_exists_atom(&mut self, negated: bool) -> Result<Expr, ParseError> {
25151        if !matches!(self.peek(), Token::LParen) {
25152            return Err(self.err(format!("expected '(' after EXISTS, got {:?}", self.peek())));
25153        }
25154        self.advance();
25155        // v7.39 (round 151) — `EXISTS (WITH … SELECT …)` is legal PG.
25156        let s = if self.peek_is_with_kw() {
25157            self.advance();
25158            self.parse_nested_with_select()?
25159        } else {
25160            let inner = self.parse_select_stmt()?;
25161            let Statement::Select(s) = inner else {
25162                unreachable!("parse_select_stmt returns Select")
25163            };
25164            s
25165        };
25166        if !matches!(self.peek(), Token::RParen) {
25167            return Err(self.err(format!(
25168                "expected ')' after EXISTS-subquery, got {:?}",
25169                self.peek()
25170            )));
25171        }
25172        self.advance();
25173        Ok(Expr::Exists {
25174            subquery: Box::new(s),
25175            negated,
25176        })
25177    }
25178
25179    fn parse_in_tail(&mut self, expr: Expr, negated: bool) -> Result<Expr, ParseError> {
25180        self.advance(); // IN
25181        if !matches!(self.peek(), Token::LParen) {
25182            return Err(self.err(format!("expected '(' after IN, got {:?}", self.peek())));
25183        }
25184        self.advance();
25185        // v4.10: `IN (SELECT ...)` — subquery branch. v7.39 (round 151)
25186        // also accepts a WITH-headed subquery (`IN (WITH … SELECT …)`).
25187        if matches!(self.peek(), Token::Select) || self.peek_is_with_kw() {
25188            let s = if self.peek_is_with_kw() {
25189                self.advance();
25190                self.parse_nested_with_select()?
25191            } else {
25192                let inner = self.parse_select_stmt()?;
25193                let Statement::Select(s) = inner else {
25194                    unreachable!("parse_select_stmt always returns Statement::Select")
25195                };
25196                s
25197            };
25198            if !matches!(self.peek(), Token::RParen) {
25199                return Err(self.err(format!(
25200                    "expected ')' after IN-subquery, got {:?}",
25201                    self.peek()
25202                )));
25203            }
25204            self.advance();
25205            return Ok(Expr::InSubquery {
25206                expr: Box::new(expr),
25207                subquery: Box::new(s),
25208                negated,
25209            });
25210        }
25211        let mut elements = Vec::new();
25212        if !matches!(self.peek(), Token::RParen) {
25213            loop {
25214                elements.push(self.parse_expr(0)?);
25215                match self.peek() {
25216                    Token::Comma => {
25217                        self.advance();
25218                    }
25219                    Token::RParen => break,
25220                    other => {
25221                        return Err(
25222                            self.err(format!("expected ',' or ')' in IN list, got {other:?}"))
25223                        );
25224                    }
25225                }
25226            }
25227        }
25228        self.advance(); // ')'
25229        // v7.30.2 (mailrs round-25) — flat InList node instead of a
25230        // left-deep OR-Eq chain: chain depth scaled with the element
25231        // count and overflowed the stack (eval + drop are recursive).
25232        if elements.is_empty() {
25233            return Ok(maybe_not(Expr::Literal(Literal::Bool(false)), negated));
25234        }
25235        Ok(Expr::InList {
25236            expr: Box::new(expr),
25237            list: elements,
25238            negated,
25239        })
25240    }
25241
25242    /// Parse a pgvector array literal `[ x1, x2, ... ]`. The opening `[` is
25243    /// already consumed by the caller. Elements must be numeric literals
25244    /// (with optional unary `-`); any compound expression is rejected at
25245    /// parse time so the runtime never needs to evaluate inside a vector.
25246    /// `EXTRACT(<field> FROM <source>)`. The dispatching `parse_atom`
25247    /// has already consumed the `EXTRACT` token before calling us —
25248    /// we pick up at the opening `(`.
25249    /// v7.17.0 Phase 2.2 — MySQL `MATCH(col [, col ...]) AGAINST
25250    /// (expr [IN BOOLEAN MODE | IN NATURAL LANGUAGE MODE
25251    /// [WITH QUERY EXPANSION]])`. Rewritten in-place to a
25252    /// per-column OR-fold of
25253    /// `to_tsvector('simple', col) @@ plainto_tsquery('simple',
25254    /// term)` so the existing FTS evaluator handles semantics.
25255    ///
25256    /// The mode modifier is accepted-and-ignored at v7.17 — all
25257    /// modes map to the same `plainto_tsquery` rewrite. Boolean-
25258    /// mode operators (`+foo -bar`) would need their own parser
25259    /// (Phase 2.2c); customers who hit them today already get a
25260    /// correct lexeme-match against the bare term, only without
25261    /// the +/- precedence the customer asked for.
25262    fn parse_match_against_atom(&mut self) -> Result<Expr, ParseError> {
25263        // Already at `MATCH`-consumed position; the dispatcher
25264        // confirmed the next token is `(`.
25265        if !matches!(self.peek(), Token::LParen) {
25266            return Err(self.err(alloc::format!(
25267                "expected '(' after MATCH, got {:?}",
25268                self.peek()
25269            )));
25270        }
25271        self.advance();
25272        let mut cols: Vec<Expr> = Vec::new();
25273        loop {
25274            cols.push(self.parse_expr(0)?);
25275            match self.peek() {
25276                Token::Comma => {
25277                    self.advance();
25278                }
25279                Token::RParen => break,
25280                other => {
25281                    return Err(self.err(alloc::format!(
25282                        "expected ',' or ')' in MATCH column list, got {other:?}"
25283                    )));
25284                }
25285            }
25286        }
25287        self.advance(); // ')'
25288        // Expect AGAINST.
25289        match self.peek() {
25290            Token::Ident(s) | Token::QuotedIdent(s) if s.eq_ignore_ascii_case("against") => {
25291                self.advance();
25292            }
25293            other => {
25294                return Err(self.err(alloc::format!(
25295                    "expected AGAINST after MATCH column list, got {other:?}"
25296                )));
25297            }
25298        }
25299        if !matches!(self.peek(), Token::LParen) {
25300            return Err(self.err(alloc::format!(
25301                "expected '(' after AGAINST, got {:?}",
25302                self.peek()
25303            )));
25304        }
25305        self.advance();
25306        // Read AGAINST's argument as a single primary token —
25307        // string literal, placeholder, or column-ref ident. We
25308        // can't call `parse_expr` / `parse_unary` here because
25309        // the postfix chain inside `parse_atom` would greedily
25310        // fold a trailing `IN BOOLEAN MODE` as `expr IN (...)`
25311        // and fail at "expected '(' after IN". Customers always
25312        // write a literal or bound parameter in AGAINST, so this
25313        // restriction is non-blocking; the error path explains
25314        // the limit if a more complex expression shows up.
25315        let term = match self.advance() {
25316            Token::String(s) => Expr::Literal(crate::ast::Literal::String(s)),
25317            Token::Placeholder(n) => Expr::Placeholder(n),
25318            Token::Ident(s) | Token::QuotedIdent(s) => Expr::Column(crate::ast::ColumnName {
25319                qualifier: None,
25320                name: s,
25321            }),
25322            other => {
25323                return Err(self.err(alloc::format!(
25324                    "MATCH ... AGAINST(<term>) expects a string literal, \
25325                     bound parameter, or column ref, got {other:?}"
25326                )));
25327            }
25328        };
25329        // Optional mode tail — accept-and-ignore at v7.17:
25330        //   IN NATURAL LANGUAGE MODE [WITH QUERY EXPANSION]
25331        //   IN BOOLEAN MODE
25332        //   WITH QUERY EXPANSION
25333        loop {
25334            match self.peek() {
25335                // IN lexes as a reserved Token::In, not an ident,
25336                // so it gets its own arm.
25337                Token::In => {
25338                    self.advance();
25339                }
25340                Token::Ident(s) | Token::QuotedIdent(s)
25341                    if s.eq_ignore_ascii_case("natural")
25342                        || s.eq_ignore_ascii_case("language")
25343                        || s.eq_ignore_ascii_case("boolean")
25344                        || s.eq_ignore_ascii_case("mode")
25345                        || s.eq_ignore_ascii_case("with")
25346                        || s.eq_ignore_ascii_case("query")
25347                        || s.eq_ignore_ascii_case("expansion") =>
25348                {
25349                    self.advance();
25350                }
25351                _ => break,
25352            }
25353        }
25354        if !matches!(self.peek(), Token::RParen) {
25355            return Err(self.err(alloc::format!(
25356                "expected ')' to close AGAINST, got {:?}",
25357                self.peek()
25358            )));
25359        }
25360        self.advance();
25361        // Build per-column `to_tsvector('simple', col) @@
25362        // plainto_tsquery('simple', term)` and OR-fold.
25363        let simple_lit = || Expr::Literal(crate::ast::Literal::String(String::from("simple")));
25364        let plainto = Expr::FunctionCall {
25365            name: String::from("plainto_tsquery"),
25366            args: alloc::vec![simple_lit(), term.clone()],
25367        };
25368        let mut folded: Option<Expr> = None;
25369        for col in cols {
25370            let to_tsv = Expr::FunctionCall {
25371                name: String::from("to_tsvector"),
25372                args: alloc::vec![simple_lit(), col],
25373            };
25374            let leaf = Expr::Binary {
25375                lhs: Box::new(to_tsv),
25376                op: crate::ast::BinOp::TsMatch,
25377                rhs: Box::new(plainto.clone()),
25378            };
25379            folded = Some(match folded {
25380                None => leaf,
25381                Some(prev) => Expr::Binary {
25382                    lhs: Box::new(prev),
25383                    op: crate::ast::BinOp::Or,
25384                    rhs: Box::new(leaf),
25385                },
25386            });
25387        }
25388        match folded {
25389            Some(e) => Ok(e),
25390            None => Err(self.err(String::from(
25391                "MATCH(...) AGAINST(...) requires at least one column",
25392            ))),
25393        }
25394    }
25395
25396    fn parse_extract_atom(&mut self) -> Result<Expr, ParseError> {
25397        if !matches!(self.peek(), Token::LParen) {
25398            return Err(self.err(format!("expected '(' after EXTRACT, got {:?}", self.peek())));
25399        }
25400        self.advance();
25401        let field_name = self.expect_ident_like()?;
25402        let field = match field_name.to_ascii_lowercase().as_str() {
25403            // PG accepts the plural spellings (years/months/…/millenniums) as
25404            // aliases for the singular fields — its datetime unit table has both.
25405            // (quarter has no plural; dow/doy/isoyear/epoch/julian likewise.)
25406            "year" | "years" => ExtractField::Year,
25407            "month" | "months" => ExtractField::Month,
25408            "day" | "days" => ExtractField::Day,
25409            "hour" | "hours" => ExtractField::Hour,
25410            "minute" | "minutes" => ExtractField::Minute,
25411            "second" | "seconds" => ExtractField::Second,
25412            "microsecond" | "microseconds" => ExtractField::Microsecond,
25413            "epoch" => ExtractField::Epoch,
25414            "dow" => ExtractField::Dow,
25415            "isodow" => ExtractField::Isodow,
25416            "doy" => ExtractField::Doy,
25417            "week" | "weeks" => ExtractField::Week,
25418            "isoyear" => ExtractField::Isoyear,
25419            "quarter" => ExtractField::Quarter,
25420            "decade" | "decades" => ExtractField::Decade,
25421            "century" | "centuries" => ExtractField::Century,
25422            "millennium" | "millenniums" | "millennia" => ExtractField::Millennium,
25423            "julian" => ExtractField::Julian,
25424            "millisecond" | "milliseconds" => ExtractField::Millisecond,
25425            "timezone" => ExtractField::Timezone,
25426            "timezone_hour" => ExtractField::TimezoneHour,
25427            "timezone_minute" => ExtractField::TimezoneMinute,
25428            // v7.39 (round 253) — PG resolves EXTRACT fields at runtime and
25429            // reports an unknown one with the source type (22023); carry the
25430            // raw name so eval can word it.
25431            other => ExtractField::Other(alloc::string::String::from(other)),
25432        };
25433        if !matches!(self.peek(), Token::From) {
25434            return Err(self.err(format!(
25435                "expected FROM after EXTRACT field, got {:?}",
25436                self.peek()
25437            )));
25438        }
25439        self.advance();
25440        let source = self.parse_expr(0)?;
25441        if !matches!(self.peek(), Token::RParen) {
25442            return Err(self.err(format!(
25443                "expected ')' to close EXTRACT, got {:?}",
25444                self.peek()
25445            )));
25446        }
25447        self.advance();
25448        Ok(Expr::Extract {
25449            field,
25450            source: Box::new(source),
25451        })
25452    }
25453
25454    /// `INTERVAL '<n> <unit> [<n> <unit> ...]'` — the `INTERVAL` keyword
25455    /// is already consumed; we expect a single string literal next and
25456    /// resolve it into `Literal::Interval` at parse time so the engine
25457    /// never has to re-tokenise inside the string.
25458    /// The unquoted count of a MySQL `INTERVAL <n> <UNIT>`, when the
25459    /// tokens ahead really are one. A quoted count (`INTERVAL '2' DAY`)
25460    /// is the SQL-standard form and is left to the path below.
25461    fn peek_unquoted_interval_count(&self) -> Option<(alloc::string::String, usize)> {
25462        // A negative count lexes as `-` then the number (`INTERVAL -1 DAY`).
25463        let (offset, sign) = match self.peek() {
25464            Token::Minus => (1, "-"),
25465            _ => (0, ""),
25466        };
25467        let Some(Token::Integer(n)) = self.tokens.get(self.pos + offset) else {
25468            return None;
25469        };
25470        self.tokens
25471            .get(self.pos + offset + 1)
25472            .filter(|t| mysql_interval_unit(t).is_some())?;
25473        Some((alloc::format!("{sign}{n}"), offset + 1))
25474    }
25475
25476    /// v7.39 (round 422) — is the parenthesised group starting at the CURRENT
25477    /// `(` a single quantity followed by a time unit (`INTERVAL (1+1) DAY`),
25478    /// rather than the argument list of MySQL's `INTERVAL(N, N1, …)` function?
25479    ///
25480    /// Scans `self.tokens` by index and consumes NOTHING. Round 409 decided
25481    /// this by parsing the group and then restoring `self.pos` — which could
25482    /// never have worked, because `advance()` DESTROYS the token it returns
25483    /// (`mem::replace(.., Eof)`); the restore yielded a stream of Eof. It was
25484    /// inert only because both branches errored back then.
25485    fn interval_paren_is_quantity(&self) -> bool {
25486        let mut depth = 0usize;
25487        let mut saw_top_level_comma = false;
25488        let mut i = self.pos;
25489        while let Some(tok) = self.tokens.get(i) {
25490            match tok {
25491                Token::LParen => depth += 1,
25492                Token::RParen => {
25493                    depth = depth.saturating_sub(1);
25494                    if depth == 0 {
25495                        return !saw_top_level_comma
25496                            && mysql_interval_unit(self.tokens.get(i + 1).unwrap_or(&Token::Eof))
25497                                .is_some();
25498                    }
25499                }
25500                // A comma directly inside the outermost parens means the
25501                // argument list of the INTERVAL() function.
25502                Token::Comma if depth == 1 => saw_top_level_comma = true,
25503                Token::Eof => return false,
25504                _ => {}
25505            }
25506            i += 1;
25507        }
25508        false
25509    }
25510
25511    fn parse_interval_atom(&mut self) -> Result<Expr, ParseError> {
25512        // v7.39 (round 409) — MySQL's `INTERVAL(N, N1, N2, …)` function
25513        // (the index of the last Ni ≤ N), distinct from the interval literal.
25514        // `INTERVAL (` is ambiguous with `INTERVAL (expr) UNIT`, so the shape
25515        // is decided by a non-destructive lookahead (round 422) before either
25516        // branch consumes anything. MySQL only.
25517        if self.mysql_dialect
25518            && matches!(self.peek(), Token::LParen)
25519            && !self.interval_paren_is_quantity()
25520        {
25521            self.advance(); // (
25522            let mut args = Vec::new();
25523            if !matches!(self.peek(), Token::RParen) {
25524                loop {
25525                    args.push(self.parse_expr(0)?);
25526                    if matches!(self.peek(), Token::Comma) {
25527                        self.advance();
25528                        continue;
25529                    }
25530                    break;
25531                }
25532            }
25533            if !matches!(self.peek(), Token::RParen) {
25534                return Err(self.err(alloc::format!(
25535                    "expected ')' after INTERVAL() arguments, got {:?}",
25536                    self.peek()
25537                )));
25538            }
25539            self.advance(); // )
25540            return Ok(Expr::FunctionCall {
25541                name: alloc::string::String::from("interval"),
25542                args,
25543            });
25544        }
25545        // v7.39 (round 350, M7) — MySQL's `INTERVAL <n> <UNIT>`, with the
25546        // number UNQUOTED: `DATE_ADD(d, INTERVAL 1 MONTH)`,
25547        // `d + INTERVAL 90 MINUTE`, `INTERVAL -1 DAY`. It is how MySQL
25548        // writes every date arithmetic there is, and it did not parse at
25549        // all. PG rejects the unquoted form outright (`syntax error at or
25550        // near "1"`, measured), so it is taken only in the MySQL dialect —
25551        // PG's own `INTERVAL '1' DAY` is untouched below.
25552        if self.mysql_dialect
25553            && let Some((text, consume)) = self.peek_unquoted_interval_count()
25554        {
25555            for _ in 0..consume {
25556                self.advance(); // the optional `-` and the number
25557            }
25558            let Some(unit) = mysql_interval_unit(self.peek()) else {
25559                return Err(self.err(alloc::format!(
25560                    "expected an interval unit after INTERVAL {text}, got {:?}",
25561                    self.peek()
25562                )));
25563            };
25564            self.advance(); // the unit
25565            let (months, days, micros) = scale_mysql_interval(&text, unit)
25566                .ok_or_else(|| self.err(alloc::format!("cannot read INTERVAL {text} {unit}")))?;
25567            return Ok(Expr::Literal(Literal::Interval {
25568                months,
25569                days,
25570                micros,
25571                // The canonical rendering, so Display round-trips into a
25572                // form both dialects read back.
25573                text: alloc::format!("{text} {unit}"),
25574            }));
25575        }
25576        // v7.39 (round 422) — MySQL's interval QUANTITY may be any expression,
25577        // not just a literal: `DATE_ADD(d, INTERVAL n DAY)`,
25578        // `d + INTERVAL n*2 DAY`, `INTERVAL (1+1) DAY`, `INTERVAL ABS(-5) DAY`.
25579        // Those cannot fold into a compile-time `Literal::Interval`, so they
25580        // lower onto the existing `make_interval(y, mo, w, d, h, mi, s)`
25581        // builtin, which builds the value at run time (and yields NULL for a
25582        // NULL quantity, as MariaDB does). The literal path above still folds
25583        // the constant case — it is cheaper and round-trips through Display.
25584        //
25585        // Guarded off a String operand so PG's own `INTERVAL '1 day'` (and
25586        // MySQL's quoted spelling) keep the qualifier path below.
25587        if self.mysql_dialect && !matches!(self.peek(), Token::String(_)) {
25588            let qty = self.parse_expr(0)?;
25589            let Some(unit) = mysql_interval_unit(self.peek()) else {
25590                return Err(self.err(alloc::format!(
25591                    "expected an interval unit after INTERVAL <expr>, got {:?}",
25592                    self.peek()
25593                )));
25594            };
25595            self.advance(); // the unit
25596            return Ok(make_interval_call(qty, unit));
25597        }
25598        let tok = self.advance();
25599        let Token::String(text) = tok else {
25600            return Err(self.err(format!(
25601                "expected string literal after INTERVAL, got {tok:?}"
25602            )));
25603        };
25604        // v7.39 (read01 round 102) — SQL-standard trailing field qualifier
25605        // `<FIELD> [TO <FIELD>]` (`INTERVAL '2' YEAR`, `INTERVAL '1-6' YEAR TO
25606        // MONTH`, `INTERVAL '1 2:03:04' DAY TO SECOND`). It sets which field a
25607        // bare number means and the leading/trailing precision.
25608        let field1 = interval_field_of(self.peek());
25609        let qualifier = if let Some(f1) = field1 {
25610            self.advance();
25611            let f2 = if matches!(self.peek(), Token::To) {
25612                self.advance();
25613                let Some(f) = interval_field_of(self.peek()) else {
25614                    return Err(self.err(format!(
25615                        "expected an interval field after TO, got {:?}",
25616                        self.peek()
25617                    )));
25618                };
25619                self.advance();
25620                Some(f)
25621            } else {
25622                None
25623            };
25624            Some((f1, f2))
25625        } else {
25626            None
25627        };
25628        let (months, days, micros) = match qualifier {
25629            Some(q) => interpret_qualified_interval(&text, q),
25630            None => parse_interval_text(&text),
25631        }
25632        .ok_or_else(|| ParseError {
25633            message: format!(
25634                "cannot parse INTERVAL {text:?}; \
25635                     expected `<n> <unit> [<n> <unit> ...]` with units \
25636                     microsecond[s], millisecond[s], second[s], minute[s], \
25637                     hour[s], day[s], week[s], month[s], year[s]"
25638            ),
25639            token_pos: self.consumed_pos(),
25640        })?;
25641        Ok(Expr::Literal(Literal::Interval {
25642            months,
25643            days,
25644            micros,
25645            text,
25646        }))
25647    }
25648
25649    /// v7.38 (read01, T10) — parse a bracketed sub-array `[e, e, …]` inside an
25650    /// `ARRAY[...]` constructor, recursing on further nested `[...]` so
25651    /// `ARRAY[[1,2],[3,4]]` (and deeper) becomes nested `Expr::Array` rather
25652    /// than a pgvector literal.
25653    fn parse_array_bracket_body(&mut self) -> Result<Expr, ParseError> {
25654        self.advance(); // consume `[`
25655        let mut items: Vec<Expr> = Vec::new();
25656        if !matches!(self.peek(), Token::RBracket) {
25657            loop {
25658                if matches!(self.peek(), Token::LBracket) {
25659                    items.push(self.parse_array_bracket_body()?);
25660                } else {
25661                    items.push(self.parse_expr(0)?);
25662                }
25663                match self.peek() {
25664                    Token::Comma => {
25665                        self.advance();
25666                    }
25667                    Token::RBracket => break,
25668                    other => {
25669                        return Err(self.err(alloc::format!(
25670                            "expected ',' or ']' in array literal, got {other:?}"
25671                        )));
25672                    }
25673                }
25674            }
25675        }
25676        self.advance(); // consume `]`
25677        Ok(Expr::Array(items))
25678    }
25679
25680    fn parse_vector_literal_body(&mut self) -> Result<Expr, ParseError> {
25681        let mut elems = Vec::new();
25682        if matches!(self.peek(), Token::RBracket) {
25683            self.advance();
25684            return Ok(Expr::Literal(Literal::Vector(elems)));
25685        }
25686        loop {
25687            let e = self.parse_expr(0)?;
25688            let x = extract_numeric_literal(&e).ok_or_else(|| ParseError {
25689                message: format!("vector element must be a numeric literal, got {e:?}"),
25690                token_pos: self.pos,
25691            })?;
25692            elems.push(x);
25693            match self.peek() {
25694                Token::Comma => {
25695                    self.advance();
25696                }
25697                Token::RBracket => {
25698                    self.advance();
25699                    break;
25700                }
25701                other => {
25702                    return Err(self.err(format!("expected ',' or ']' in vector, got {other:?}")));
25703                }
25704            }
25705        }
25706        Ok(Expr::Literal(Literal::Vector(elems)))
25707    }
25708
25709    /// Atom that started with an identifier: could be `t.col`, `col`, or
25710    /// `func(arg, ...)`. Detect each shape by looking at the next token.
25711    /// v4.12: parse `(PARTITION BY expr, ... ORDER BY expr [DESC]
25712    /// [, ...])`. Caller has already consumed `OVER`. Either clause
25713    /// is optional; an empty `()` is also legal (PG semantics).
25714    /// v6.4.2 — consume an optional `IGNORE NULLS` / `RESPECT NULLS`
25715    /// modifier between `name(args)` and `OVER (...)`. Default is
25716    /// `Respect`. Unrecognised idents leave the stream unchanged.
25717    fn parse_null_treatment_modifier(&mut self) -> NullTreatment {
25718        let Token::Ident(s) = self.peek().clone() else {
25719            return NullTreatment::Respect;
25720        };
25721        let is_ignore = s.eq_ignore_ascii_case("ignore");
25722        let is_respect = s.eq_ignore_ascii_case("respect");
25723        if !is_ignore && !is_respect {
25724            return NullTreatment::Respect;
25725        }
25726        // Lookahead for NULLS — only consume both tokens together.
25727        // pos+1 must hold a "nulls" ident.
25728        if self.pos + 1 < self.tokens.len()
25729            && let Token::Ident(s2) = &self.tokens[self.pos + 1]
25730            && s2.eq_ignore_ascii_case("nulls")
25731        {
25732            self.advance();
25733            self.advance();
25734            return if is_ignore {
25735                NullTreatment::Ignore
25736            } else {
25737                NullTreatment::Respect
25738            };
25739        }
25740        NullTreatment::Respect
25741    }
25742
25743    /// v7.32 (mailrs round-29) — `agg(args) FILTER (WHERE cond)`.
25744    /// `FILTER` is an unreserved keyword, so it arrives as an `Ident`
25745    /// (same shape as the `OVER` tail). Consumes the whole clause and
25746    /// returns the predicate; returns `None` when no `FILTER` follows.
25747    fn parse_filter_clause(&mut self) -> Result<Option<Box<Expr>>, ParseError> {
25748        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25749            return Ok(None);
25750        };
25751        if !s.eq_ignore_ascii_case("filter") {
25752            return Ok(None);
25753        }
25754        self.advance(); // FILTER
25755        if !matches!(self.peek(), Token::LParen) {
25756            return Err(self.err(format!("expected '(' after FILTER, got {:?}", self.peek())));
25757        }
25758        self.advance(); // (
25759        if !matches!(self.peek(), Token::Where) {
25760            return Err(self.err(format!(
25761                "expected WHERE inside FILTER (...), got {:?}",
25762                self.peek()
25763            )));
25764        }
25765        self.advance(); // WHERE
25766        let cond = self.parse_expr(0)?;
25767        if !matches!(self.peek(), Token::RParen) {
25768            return Err(self.err(format!(
25769                "expected ')' to close FILTER (WHERE ...), got {:?}",
25770                self.peek()
25771            )));
25772        }
25773        self.advance(); // )
25774        Ok(Some(Box::new(cond)))
25775    }
25776
25777    /// v7.39 (round 354, M12) — consume a `SEPARATOR '<s>'` tail and push
25778    /// the separator as the aggregate's second argument, which is the
25779    /// shape `string_agg` already takes. Returns whether one was there.
25780    fn consume_group_concat_separator(&mut self, args: &mut Vec<Expr>) -> Result<bool, ParseError> {
25781        if !matches!(self.peek(), Token::Ident(k) if k.eq_ignore_ascii_case("separator")) {
25782            return Ok(false);
25783        }
25784        self.advance();
25785        let Token::String(sep) = self.peek().clone() else {
25786            return Err(self.err(alloc::format!(
25787                "expected a string literal after SEPARATOR, got {:?}",
25788                self.peek()
25789            )));
25790        };
25791        self.advance();
25792        args.push(Expr::Literal(Literal::String(sep)));
25793        Ok(true)
25794    }
25795
25796    /// v7.32 (round-29) — `WITHIN GROUP ( ORDER BY <sort_spec> )` tail
25797    /// for ordered-set aggregates. `WITHIN` is unreserved (arrives as an
25798    /// `Ident`); `GROUP` and `ORDER`/`BY` are keywords. Returns the sort
25799    /// keys, or an empty vec when no `WITHIN GROUP` follows.
25800    fn parse_within_group_clause(&mut self) -> Result<Vec<OrderBy>, ParseError> {
25801        let (Token::Ident(s) | Token::QuotedIdent(s)) = self.peek() else {
25802            return Ok(Vec::new());
25803        };
25804        if !s.eq_ignore_ascii_case("within") {
25805            return Ok(Vec::new());
25806        }
25807        self.advance(); // WITHIN
25808        if !matches!(self.peek(), Token::Group) {
25809            return Err(self.err(format!(
25810                "expected GROUP after WITHIN, got {:?}",
25811                self.peek()
25812            )));
25813        }
25814        self.advance(); // GROUP
25815        if !matches!(self.peek(), Token::LParen) {
25816            return Err(self.err(format!(
25817                "expected '(' after WITHIN GROUP, got {:?}",
25818                self.peek()
25819            )));
25820        }
25821        self.advance(); // (
25822        if !matches!(self.peek(), Token::Order) {
25823            return Err(self.err(format!(
25824                "expected ORDER BY inside WITHIN GROUP (...), got {:?}",
25825                self.peek()
25826            )));
25827        }
25828        self.advance(); // ORDER
25829        if !self.peek_is_by() {
25830            return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25831        }
25832        self.advance(); // BY
25833        let mut keys: Vec<OrderBy> = Vec::new();
25834        loop {
25835            // v7.39 (round 691) — save/restore, the discipline this parser
25836            // already uses around `pending_sample_preds`, so a subquery inside
25837            // a key neither inherits nor leaks the channel.
25838            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
25839            let saved_coll = self.order_key_collation.take();
25840            let parsed = self.parse_expr(0);
25841            self.in_order_by_key = saved_flag;
25842            let collation = core::mem::replace(&mut self.order_key_collation, saved_coll);
25843            let expr = parsed?;
25844            let desc = if matches!(self.peek(), Token::Desc) {
25845                self.advance();
25846                true
25847            } else if matches!(self.peek(), Token::Asc) {
25848                self.advance();
25849                false
25850            } else {
25851                false
25852            };
25853            let nulls_first = self.parse_optional_nulls_placement()?;
25854            keys.push(OrderBy {
25855                expr,
25856                desc,
25857                nulls_first,
25858                collation,
25859            });
25860            if matches!(self.peek(), Token::Comma) {
25861                self.advance();
25862            } else {
25863                break;
25864            }
25865        }
25866        if !matches!(self.peek(), Token::RParen) {
25867            return Err(self.err(format!(
25868                "expected ')' to close WITHIN GROUP (ORDER BY ...), got {:?}",
25869                self.peek()
25870            )));
25871        }
25872        self.advance(); // )
25873        Ok(keys)
25874    }
25875
25876    /// No frame clause is supported.
25877    #[allow(clippy::type_complexity)] // (partitions, ordered-keys-with-desc) is the natural shape
25878    fn parse_over_clause(
25879        &mut self,
25880    ) -> Result<
25881        (
25882            Vec<Expr>,
25883            Vec<(Expr, bool, Option<bool>)>,
25884            Option<WindowFrame>,
25885        ),
25886        ParseError,
25887    > {
25888        // `OVER w` — a named-window reference. The WINDOW clause
25889        // parses after the select list, so the name rides out as a
25890        // marker in partition_by; parse_bare_select substitutes the
25891        // definition once the clause is known.
25892        if let Token::Ident(w) | Token::QuotedIdent(w) = self.peek() {
25893            let name = w.clone();
25894            self.advance();
25895            return Ok((
25896                alloc::vec![Expr::Column(crate::ast::ColumnName {
25897                    qualifier: Some("__named_window__".to_string()),
25898                    name,
25899                })],
25900                Vec::new(),
25901                None,
25902            ));
25903        }
25904        if !matches!(self.peek(), Token::LParen) {
25905            return Err(self.err(format!("expected '(' after OVER, got {:?}", self.peek())));
25906        }
25907        self.advance();
25908        let mut partition_by = Vec::new();
25909        let mut order_by = Vec::new();
25910        // v7.39 (round 229) — `OVER (w1 …)`: a *copy* of an existing named
25911        // window, refined in place. PG's rules (probed against 18.4) differ
25912        // from the bare `OVER w1` form, so the reference rides out under its
25913        // own marker and `substitute_named_windows` applies them. The base
25914        // name is any leading identifier that isn't a window-spec keyword.
25915        let base_window = match self.peek() {
25916            Token::Ident(s) | Token::QuotedIdent(s)
25917                if !s.eq_ignore_ascii_case("partition")
25918                    && !s.eq_ignore_ascii_case("rows")
25919                    && !s.eq_ignore_ascii_case("range")
25920                    && !s.eq_ignore_ascii_case("groups") =>
25921            {
25922                let n = s.clone();
25923                self.advance();
25924                Some(n)
25925            }
25926            _ => None,
25927        };
25928        // PARTITION BY ?
25929        // v7.37.6-B promoted PARTITION to a reserved keyword
25930        // (Token::Partition); pre-7.37.6-B catalogs lexed it as
25931        // `Token::Ident("partition")`. Accept both so older sources
25932        // and the new lexer surface land on the same path.
25933        let is_partition_kw = match self.peek() {
25934            Token::Partition => true,
25935            Token::Ident(s) | Token::QuotedIdent(s) => s.eq_ignore_ascii_case("partition"),
25936            _ => false,
25937        };
25938        if is_partition_kw {
25939            self.advance();
25940            if !self.peek_is_by() {
25941                return Err(self.err(format!(
25942                    "expected BY after PARTITION, got {:?}",
25943                    self.peek()
25944                )));
25945            }
25946            self.advance();
25947            loop {
25948                partition_by.push(self.parse_expr(0)?);
25949                if matches!(self.peek(), Token::Comma) {
25950                    self.advance();
25951                    continue;
25952                }
25953                break;
25954            }
25955        }
25956        // ORDER BY ?
25957        if matches!(self.peek(), Token::Order) {
25958            self.advance();
25959            if !self.peek_is_by() {
25960                return Err(self.err(format!("expected BY after ORDER, got {:?}", self.peek())));
25961            }
25962            self.advance();
25963            loop {
25964                let e = self.parse_expr(0)?;
25965                let desc = if matches!(self.peek(), Token::Desc) {
25966                    self.advance();
25967                    true
25968                } else if matches!(self.peek(), Token::Asc) {
25969                    self.advance();
25970                    false
25971                } else {
25972                    false
25973                };
25974                // v7.24.1 — NULLS FIRST/LAST inside OVER (…).
25975                let nulls_first = self.parse_optional_nulls_placement()?;
25976                order_by.push((e, desc, nulls_first));
25977                if matches!(self.peek(), Token::Comma) {
25978                    self.advance();
25979                    continue;
25980                }
25981                break;
25982            }
25983        }
25984        // v4.20: optional explicit frame, `ROWS ...` / `RANGE ...`.
25985        // Both keywords come through the lexer as identifiers; match
25986        // case-insensitively.
25987        let mut frame: Option<WindowFrame> = None;
25988        if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek() {
25989            let kind = if s.eq_ignore_ascii_case("rows") {
25990                Some(FrameKind::Rows)
25991            } else if s.eq_ignore_ascii_case("range") {
25992                Some(FrameKind::Range)
25993            } else if s.eq_ignore_ascii_case("groups") {
25994                // v7.37.19 (19.11) — PG 11+ GROUPS frame mode.
25995                Some(FrameKind::Groups)
25996            } else {
25997                None
25998            };
25999            if let Some(kind) = kind {
26000                self.advance();
26001                frame = Some(self.parse_frame_tail(kind)?);
26002            }
26003        }
26004        if !matches!(self.peek(), Token::RParen) {
26005            return Err(self.err(format!(
26006                "expected ')' to close OVER clause, got {:?}",
26007                self.peek()
26008            )));
26009        }
26010        self.advance();
26011        if let Some(base) = base_window {
26012            // A copy may refine but never override the base's partitioning
26013            // (PG rejects it outright, before looking the name up).
26014            if !partition_by.is_empty() {
26015                return Err(self.err(alloc::format!(
26016                    "cannot override PARTITION BY clause of window \"{base}\""
26017                )));
26018            }
26019            partition_by = alloc::vec![Expr::Column(crate::ast::ColumnName {
26020                qualifier: Some("__named_window_ref__".to_string()),
26021                name: base,
26022            })];
26023        }
26024        Ok((partition_by, order_by, frame))
26025    }
26026
26027    /// v4.20: parse the tail of an explicit frame, given the `ROWS`
26028    /// or `RANGE` keyword was just consumed. Accepts both
26029    /// `BETWEEN <bound> AND <bound>` and the single-bound shorthand
26030    /// (`ROWS UNBOUNDED PRECEDING`, `ROWS 5 PRECEDING`, etc.) which
26031    /// PG normalises to `BETWEEN <bound> AND CURRENT ROW`.
26032    fn parse_frame_tail(&mut self, kind: FrameKind) -> Result<WindowFrame, ParseError> {
26033        let (start, end) = if matches!(self.peek(), Token::Between) {
26034            self.advance();
26035            let start = self.parse_frame_bound()?;
26036            if !matches!(self.peek(), Token::And) {
26037                return Err(self.err(format!("expected AND in frame spec, got {:?}", self.peek())));
26038            }
26039            self.advance();
26040            let end = self.parse_frame_bound()?;
26041            (start, Some(end))
26042        } else {
26043            (self.parse_frame_bound()?, None)
26044        };
26045        let exclude = self.parse_frame_exclusion()?;
26046        Ok(WindowFrame {
26047            kind,
26048            start,
26049            end,
26050            exclude,
26051        })
26052    }
26053
26054    /// Optional `EXCLUDE {CURRENT ROW | GROUP | TIES | NO OTHERS}`
26055    /// after a frame spec. NO OTHERS is the default no-op.
26056    fn parse_frame_exclusion(&mut self) -> Result<FrameExclusion, ParseError> {
26057        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("exclude")) {
26058            return Ok(FrameExclusion::NoOthers);
26059        }
26060        self.advance(); // EXCLUDE
26061        match self.peek() {
26062            Token::Ident(s) if s.eq_ignore_ascii_case("current") => {
26063                self.advance();
26064                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("row")) {
26065                    return Err(self.err(format!(
26066                        "expected ROW after EXCLUDE CURRENT, got {:?}",
26067                        self.peek()
26068                    )));
26069                }
26070                self.advance();
26071                Ok(FrameExclusion::CurrentRow)
26072            }
26073            // v7.39 (read01 round 109) — GROUP is a reserved keyword token, so
26074            // `EXCLUDE GROUP` arrives as `Token::Group`, not `Ident("group")`.
26075            // Without this arm it fell to the catch-all, whose message
26076            // self-contradictingly listed GROUP as expected.
26077            Token::Ident(s) if s.eq_ignore_ascii_case("group") => {
26078                self.advance();
26079                Ok(FrameExclusion::Group)
26080            }
26081            Token::Group => {
26082                self.advance();
26083                Ok(FrameExclusion::Group)
26084            }
26085            Token::Ident(s) if s.eq_ignore_ascii_case("ties") => {
26086                self.advance();
26087                Ok(FrameExclusion::Ties)
26088            }
26089            Token::Ident(s) if s.eq_ignore_ascii_case("no") => {
26090                self.advance();
26091                if !matches!(self.peek(), Token::Ident(r) if r.eq_ignore_ascii_case("others")) {
26092                    return Err(self.err(format!(
26093                        "expected OTHERS after EXCLUDE NO, got {:?}",
26094                        self.peek()
26095                    )));
26096                }
26097                self.advance();
26098                Ok(FrameExclusion::NoOthers)
26099            }
26100            other => Err(self.err(format!(
26101                "expected CURRENT ROW / GROUP / TIES / NO OTHERS after EXCLUDE, got {other:?}"
26102            ))),
26103        }
26104    }
26105
26106    /// Parse one frame bound: `UNBOUNDED PRECEDING`, `<n> PRECEDING`,
26107    /// `<interval> PRECEDING`, `CURRENT ROW`, `<n>/<interval> FOLLOWING`,
26108    /// `UNBOUNDED FOLLOWING`.
26109    fn parse_frame_bound(&mut self) -> Result<FrameBound, ParseError> {
26110        // Interval-typed offset for a value-based RANGE frame over a
26111        // DATE / TIMESTAMP ORDER BY column (PG time-series windows),
26112        // spelled `INTERVAL '1 day' PRECEDING` or `'1 day'::interval
26113        // PRECEDING`.
26114        if let Some((months, days, micros)) = self.try_take_interval_offset()? {
26115            let dir = self.expect_ident_like()?;
26116            return if dir.eq_ignore_ascii_case("preceding") {
26117                Ok(FrameBound::IntervalPreceding {
26118                    months,
26119                    days,
26120                    micros,
26121                })
26122            } else if dir.eq_ignore_ascii_case("following") {
26123                Ok(FrameBound::IntervalFollowing {
26124                    months,
26125                    days,
26126                    micros,
26127                })
26128            } else {
26129                Err(self.err(format!(
26130                    "expected PRECEDING or FOLLOWING after interval offset, got {dir:?}"
26131                )))
26132            };
26133        }
26134        // Number-led: "<n> PRECEDING" / "<n> FOLLOWING".
26135        if let Token::Integer(n) = *self.peek() {
26136            self.advance();
26137            let n: u64 = u64::try_from(n).map_err(|_| {
26138                self.err(format!(
26139                    "invalid frame offset {n} — expected non-negative integer"
26140                ))
26141            })?;
26142            let dir = self.expect_ident_like()?;
26143            return if dir.eq_ignore_ascii_case("preceding") {
26144                Ok(FrameBound::OffsetPreceding(n))
26145            } else if dir.eq_ignore_ascii_case("following") {
26146                Ok(FrameBound::OffsetFollowing(n))
26147            } else {
26148                Err(self.err(format!(
26149                    "expected PRECEDING or FOLLOWING after offset, got {dir:?}"
26150                )))
26151            };
26152        }
26153        let first = self.expect_ident_like()?;
26154        if first.eq_ignore_ascii_case("unbounded") {
26155            let dir = self.expect_ident_like()?;
26156            return if dir.eq_ignore_ascii_case("preceding") {
26157                Ok(FrameBound::UnboundedPreceding)
26158            } else if dir.eq_ignore_ascii_case("following") {
26159                Ok(FrameBound::UnboundedFollowing)
26160            } else {
26161                Err(self.err(format!(
26162                    "expected PRECEDING or FOLLOWING after UNBOUNDED, got {dir:?}"
26163                )))
26164            };
26165        }
26166        if first.eq_ignore_ascii_case("current") {
26167            let row = self.expect_ident_like()?;
26168            if !row.eq_ignore_ascii_case("row") {
26169                return Err(self.err(format!("expected ROW after CURRENT, got {row:?}")));
26170            }
26171            return Ok(FrameBound::CurrentRow);
26172        }
26173        Err(self.err(format!(
26174            "expected frame bound (UNBOUNDED/CURRENT/<n>), got {first:?}"
26175        )))
26176    }
26177
26178    /// Detect and consume a leading interval offset in a frame bound —
26179    /// `INTERVAL '1 day'` or `'1 day'::interval` — returning its folded
26180    /// `(months, days, micros)`. Leaves the cursor on the trailing
26181    /// PRECEDING / FOLLOWING keyword. Returns `None` (without advancing)
26182    /// when the next tokens are not an interval offset.
26183    fn try_take_interval_offset(&mut self) -> Result<Option<(i32, i32, i64)>, ParseError> {
26184        // Shape A — `INTERVAL '1 day'`.
26185        if matches!(self.peek(), Token::Interval) {
26186            self.advance(); // INTERVAL
26187            let atom = self.parse_interval_atom()?;
26188            if let Expr::Literal(Literal::Interval {
26189                months,
26190                days,
26191                micros,
26192                ..
26193            }) = atom
26194            {
26195                return Ok(Some((months, days, micros)));
26196            }
26197            return Err(self.err("expected an interval literal in frame offset".to_string()));
26198        }
26199        // Shape B — `'1 day'::interval`. Look ahead for the exact
26200        // string / `::` / interval-target triple before committing.
26201        if let Token::String(text) = self.peek() {
26202            let target_is_interval = match self.tokens.get(self.pos + 2) {
26203                Some(Token::Interval) => true,
26204                Some(Token::Ident(s)) => s.eq_ignore_ascii_case("interval"),
26205                _ => false,
26206            };
26207            let is_cast = matches!(self.tokens.get(self.pos + 1), Some(Token::DoubleColon))
26208                && target_is_interval;
26209            if is_cast {
26210                let text = text.clone();
26211                self.advance(); // string
26212                self.advance(); // ::
26213                self.advance(); // interval
26214                let parts = parse_interval_text(&text).ok_or_else(|| {
26215                    self.err(format!("cannot parse INTERVAL {text:?} in frame offset"))
26216                })?;
26217                return Ok(Some(parts));
26218            }
26219        }
26220        Ok(None)
26221    }
26222
26223    fn finish_ident_atom(&mut self, first: String) -> Result<Expr, ParseError> {
26224        // v7.39.2 — MySQL's charset INTRODUCER: `_utf8mb4'x'`, `N'y'`,
26225        // `_binary'z'`. All three were `ERROR 1064 syntax error` here
26226        // and all three answer the literal on MySQL 9.7.2.
26227        //
26228        // It is not only syntax, which is why it waited for
26229        // `Expr::Collate`: measured, `_binary'A' = 'a'` is 0 on MySQL
26230        // because `_binary` makes the comparison byte-wise, while
26231        // `_utf8mb4'A' = _utf8mb4'a'` is 1. Accepting the syntax and
26232        // dropping the charset would have turned a hard error into a
26233        // silently wrong comparison — worse than the error it replaced.
26234        //
26235        // An UNKNOWN charset is NOT an introducer: MySQL answers
26236        // `Unknown column '_nosuch'`, because it parses as a column
26237        // reference followed by a string. So the table decides, and it
26238        // is the same table `SET NAMES` reads.
26239        //
26240        // A space is allowed between the two (`_utf8mb4 'x'`), which
26241        // falls out of asking the token stream rather than the bytes.
26242        if self.mysql_dialect
26243            && let Token::String(_) = self.peek()
26244        {
26245            let lower = first.to_ascii_lowercase();
26246            let charset = if lower == "n" {
26247                // `N'…'` is the national character set, which MySQL
26248                // documents as utf8 — utf8mb3 in 9.7.2's spelling.
26249                //
26250                // utf8mb3 and utf8mb4 both fold case in their default
26251                // collations, so nothing SPG can be asked distinguishes
26252                // the two here: an ablation swapping this to utf8mb4
26253                // reddens no pin. Recorded rather than implied — the
26254                // spelling follows MySQL's documentation, not a
26255                // measurement.
26256                Some("utf8mb3")
26257            } else {
26258                // No filter here: the lookup below IS the test for
26259                // "is this a charset". An ablation that removed a filter
26260                // in this spot reddened nothing, which is how the two
26261                // were found to be one check written twice.
26262                lower.strip_prefix('_')
26263            };
26264            if let Some(cs) = charset
26265                && let Some(collation) = crate::charset::charset_default_collation(cs)
26266            {
26267                let Token::String(body) = self.advance() else {
26268                    unreachable!("peeked a string");
26269                };
26270                return Ok(Expr::Collate {
26271                    expr: Box::new(Expr::Literal(Literal::String(body))),
26272                    collation: String::from(collation),
26273                });
26274            }
26275        }
26276        if matches!(self.peek(), Token::Dot) {
26277            self.advance();
26278            let name = self.expect_ident_like()?;
26279            // v7.14.0 — schema-qualified function call
26280            // `<schema>.<fn>(args)`. PG dumps emit
26281            // `pg_catalog.set_config(...)` in the preamble. SPG
26282            // is single-namespace: drop the schema prefix and
26283            // route the dispatch on the bare function name.
26284            if matches!(self.peek(), Token::LParen) {
26285                return self.finish_ident_atom(name);
26286            }
26287            return Ok(Expr::Column(ColumnName {
26288                qualifier: Some(first),
26289                name,
26290            }));
26291        }
26292        if matches!(self.peek(), Token::LParen) {
26293            self.advance();
26294            // `COUNT(*)` — special-cased here because `*` isn't a normal
26295            // expression token. Lower-case match on `first` since the lexer
26296            // folds identifiers.
26297            if first.eq_ignore_ascii_case("count") && matches!(self.peek(), Token::Star) {
26298                self.advance();
26299                if !matches!(self.peek(), Token::RParen) {
26300                    return Err(self.err(format!(
26301                        "expected ')' after COUNT(*), got {:?}",
26302                        self.peek()
26303                    )));
26304                }
26305                self.advance();
26306                // v7.32 (round-29) — `COUNT(*) FILTER (WHERE …)`.
26307                let filter = self.parse_filter_clause()?;
26308                // v4.12: COUNT(*) OVER (...) — same window tail.
26309                let null_treatment = self.parse_null_treatment_modifier();
26310                if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
26311                    && s.eq_ignore_ascii_case("over")
26312                {
26313                    self.advance();
26314                    let (partition_by, order_by, frame) = self.parse_over_clause()?;
26315                    return Ok(Expr::WindowFunction {
26316                        name: "count_star".into(),
26317                        args: Vec::new(),
26318                        partition_by,
26319                        order_by,
26320                        frame,
26321                        null_treatment,
26322                        filter,
26323                    });
26324                }
26325                if let Some(filter) = filter {
26326                    return Ok(Expr::AggregateOrdered {
26327                        call: Box::new(Expr::FunctionCall {
26328                            name: "count_star".into(),
26329                            args: Vec::new(),
26330                        }),
26331                        order_by: Vec::new(),
26332                        distinct: false,
26333                        filter: Some(filter),
26334                    });
26335                }
26336                return Ok(Expr::FunctionCall {
26337                    name: "count_star".into(),
26338                    args: Vec::new(),
26339                });
26340            }
26341            // Function call. PG-style: zero-or-more comma-separated args.
26342            let mut args = Vec::new();
26343            // v7.38 (read01, T14) — named-argument notation `argname => value`.
26344            // Names are collected in lock-step with `args` and resolved to
26345            // positional order after the loop (the AST stays positional).
26346            let mut arg_names: Vec<Option<String>> = Vec::new();
26347            let mut agg_order_by: Vec<OrderBy> = Vec::new();
26348            // v7.39 (round 354, M12) — whether a `SEPARATOR '<s>'` tail was
26349            // seen, so the value arguments before it can be folded.
26350            let mut saw_separator = false;
26351            // v7.25 (round-17) — `COUNT(DISTINCT x)` and friends.
26352            // v7.32 (round-29) — accept the dual `ALL` quantifier too
26353            // (the default; ORMs emit `COUNT(ALL x)` / `SUM(ALL x)`).
26354            let agg_distinct = if matches!(self.peek(), Token::Distinct) {
26355                self.advance();
26356                true
26357            } else if matches!(self.peek(), Token::All) {
26358                self.advance();
26359                false
26360            } else {
26361                false
26362            };
26363            // v7.37.17 (17.6 siblings) — MySQL TIMESTAMPADD /
26364            // TIMESTAMPDIFF take a bare unit keyword as the first
26365            // argument (MINUTE, DAY, ...), and GET_FORMAT takes a
26366            // bare type keyword (DATE / TIME / DATETIME); lower them
26367            // onto string literals so the evaluator sees plain text.
26368            if ((first.eq_ignore_ascii_case("timestampadd")
26369                || first.eq_ignore_ascii_case("timestampdiff"))
26370                && matches!(self.peek(), Token::Ident(u) if matches!(
26371                    u.to_ascii_lowercase().as_str(),
26372                    "microsecond" | "second" | "minute" | "hour" | "day"
26373                        | "week" | "month" | "quarter" | "year"
26374                )))
26375                || (first.eq_ignore_ascii_case("get_format")
26376                    && matches!(self.peek(), Token::Ident(u) if matches!(
26377                        u.to_ascii_lowercase().as_str(),
26378                        "date" | "time" | "datetime" | "timestamp"
26379                    )))
26380            {
26381                if let Token::Ident(u) = self.peek() {
26382                    args.push(Expr::Literal(Literal::String(u.to_ascii_lowercase())));
26383                }
26384                self.advance();
26385                if matches!(self.peek(), Token::Comma) {
26386                    self.advance();
26387                }
26388            }
26389            // `ROW(a, b, …)` keyword constructor. Followed by a
26390            // comparison operator or [NOT] IN it joins the paren
26391            // row-constructor machinery (fieldwise parse-time
26392            // expansion); bare, it stays a `row` call the evaluator
26393            // renders as PG record text.
26394            if first.eq_ignore_ascii_case("row") {
26395                let mut row_items = Vec::new();
26396                if !matches!(self.peek(), Token::RParen) {
26397                    loop {
26398                        row_items.push(self.parse_expr(0)?);
26399                        match self.peek() {
26400                            Token::Comma => {
26401                                self.advance();
26402                            }
26403                            Token::RParen => break,
26404                            other => {
26405                                return Err(self.err(format!(
26406                                    "expected ',' or ')' in ROW(...), got {other:?}"
26407                                )));
26408                            }
26409                        }
26410                    }
26411                }
26412                self.advance(); // ')'
26413                let comparison_follows = matches!(
26414                    self.peek(),
26415                    Token::Eq
26416                        | Token::NotEq
26417                        | Token::Lt
26418                        | Token::LtEq
26419                        | Token::Gt
26420                        | Token::GtEq
26421                        | Token::In
26422                ) || (matches!(self.peek(), Token::Not)
26423                    && matches!(self.tokens.get(self.pos + 1), Some(Token::In)))
26424                    || matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("overlaps"));
26425                if comparison_follows && !row_items.is_empty() {
26426                    return self.parse_row_comparison_tail(row_items);
26427                }
26428                return Ok(Expr::FunctionCall {
26429                    name: String::from("row"),
26430                    args: row_items,
26431                });
26432            }
26433            // v7.39 (read01 xml.c) — `XMLPARSE(DOCUMENT|CONTENT expr)`:
26434            // the parse-mode keyword introduces the source text. SPG
26435            // carries XML as text, so both modes lower to __xmlparse(expr)
26436            // which validates well-formedness and returns Value::Xml.
26437            if first.eq_ignore_ascii_case("xmlparse")
26438                && matches!(self.peek(), Token::Ident(kw)
26439                    if kw.eq_ignore_ascii_case("document")
26440                        || kw.eq_ignore_ascii_case("content"))
26441            {
26442                let mode = match self.advance() {
26443                    Token::Ident(kw) => kw.to_ascii_lowercase(),
26444                    _ => unreachable!("peeked an ident"),
26445                };
26446                let src = self.parse_expr(0)?;
26447                if !matches!(self.peek(), Token::RParen) {
26448                    return Err(self.err(format!(
26449                        "expected ')' to close XMLPARSE, got {:?}",
26450                        self.peek()
26451                    )));
26452                }
26453                self.advance();
26454                return Ok(Expr::FunctionCall {
26455                    name: String::from("__xmlparse"),
26456                    args: alloc::vec![src, Expr::Literal(Literal::String(mode))],
26457                });
26458            }
26459            // SQL/XML `XMLELEMENT(NAME ident [, content …])` — the NAME
26460            // keyword introduces the element name (a bare or quoted
26461            // identifier), then optional content expressions. Lower to a
26462            // plain `xmlelement(name_text, content …)` call.
26463            if first.eq_ignore_ascii_case("xmlelement")
26464                && matches!(self.peek(), Token::Ident(kw) if kw.eq_ignore_ascii_case("name"))
26465            {
26466                self.advance(); // consume NAME
26467                let elem_name = match self.peek().clone() {
26468                    Token::Ident(n) | Token::QuotedIdent(n) => {
26469                        self.advance();
26470                        n
26471                    }
26472                    other => {
26473                        return Err(self.err(format!(
26474                            "expected element name after XMLELEMENT NAME, got {other:?}"
26475                        )));
26476                    }
26477                };
26478                let mut args = alloc::vec![Expr::Literal(Literal::String(elem_name))];
26479                while matches!(self.peek(), Token::Comma) {
26480                    self.advance();
26481                    args.push(self.parse_expr(0)?);
26482                }
26483                if !matches!(self.peek(), Token::RParen) {
26484                    return Err(self.err(format!(
26485                        "expected ')' to close XMLELEMENT, got {:?}",
26486                        self.peek()
26487                    )));
26488                }
26489                self.advance();
26490                return Ok(Expr::FunctionCall {
26491                    name: String::from("xmlelement"),
26492                    args,
26493                });
26494            }
26495            // SQL/XML `XMLFOREST(value [AS name], …)` — each `value AS name`
26496            // becomes a `<name>value</name>` element; a bare column infers its
26497            // own name. Lower to `xmlforest(name1, val1, name2, val2, …)`.
26498            // v7.39.2 — MySQL's two CONVERT forms, neither of which parsed.
26499            // `CONVERT(expr USING cs)` was a syntax error at USING, and
26500            // `CONVERT(expr, CHAR)` was read as PostgreSQL's three-argument
26501            // `convert(bytea, src, dest)` and answered `column "char" does
26502            // not exist`. Both are casts in MySQL: measured on 9.7.2,
26503            // `CONVERT(0x41 USING utf8mb4)` and `CONVERT(0x41, CHAR)` are
26504            // both 'A', and `CONVERT(123, CHAR)` is '123'.
26505            //
26506            // The charset is checked against the same table the introducers
26507            // use, so an unknown one is refused rather than quietly ignored.
26508            if self.mysql_dialect
26509                && first.eq_ignore_ascii_case("convert")
26510                && !matches!(self.peek(), Token::RParen)
26511            {
26512                let save = self.pos;
26513                let inner = self.parse_expr(0)?;
26514                if matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("using")) {
26515                    self.advance();
26516                    let cs = match self.peek().clone() {
26517                        Token::Ident(n) | Token::QuotedIdent(n) => {
26518                            self.advance();
26519                            n
26520                        }
26521                        other => {
26522                            return Err(self.err(alloc::format!(
26523                                "expected a charset after USING, got {other:?}"
26524                            )));
26525                        }
26526                    };
26527                    let lc = cs.to_ascii_lowercase();
26528                    if lc != "binary" && crate::charset::charset_default_collation(&lc).is_none() {
26529                        return Err(self.err(alloc::format!("unknown character set: '{cs}'")));
26530                    }
26531                    if !matches!(self.peek(), Token::RParen) {
26532                        return Err(self.err(alloc::format!(
26533                            "expected ')' after CONVERT … USING, got {:?}",
26534                            self.peek()
26535                        )));
26536                    }
26537                    self.advance();
26538                    let target = if lc == "binary" {
26539                        CastTarget::Named("binary".to_string())
26540                    } else {
26541                        CastTarget::Text
26542                    };
26543                    return self.finish_postfix_casts(Expr::Cast {
26544                        expr: alloc::boxed::Box::new(inner),
26545                        target,
26546                    });
26547                }
26548                if matches!(self.peek(), Token::Comma) {
26549                    self.advance();
26550                    // A type name here is MySQL's cast form; anything else
26551                    // (three string arguments) is PostgreSQL's `convert`,
26552                    // which keeps its own path.
26553                    if let Ok(target) = self.parse_cast_target()
26554                        && matches!(self.peek(), Token::RParen)
26555                    {
26556                        self.advance();
26557                        return self.finish_postfix_casts(Expr::Cast {
26558                            expr: alloc::boxed::Box::new(inner),
26559                            target,
26560                        });
26561                    }
26562                }
26563                self.pos = save;
26564            }
26565            if first.eq_ignore_ascii_case("xmlforest") && !matches!(self.peek(), Token::RParen) {
26566                let mut args: Vec<Expr> = Vec::new();
26567                loop {
26568                    let val = self.parse_expr(0)?;
26569                    let name = if matches!(self.peek(), Token::As) {
26570                        self.advance();
26571                        match self.peek().clone() {
26572                            Token::Ident(n) | Token::QuotedIdent(n) => {
26573                                self.advance();
26574                                n
26575                            }
26576                            other => {
26577                                return Err(self.err(format!(
26578                                    "expected name after AS in XMLFOREST, got {other:?}"
26579                                )));
26580                            }
26581                        }
26582                    } else if let Expr::Column(c) = &val {
26583                        c.name.clone()
26584                    } else {
26585                        return Err(
26586                            self.err("XMLFOREST element without a column name needs AS".into())
26587                        );
26588                    };
26589                    args.push(Expr::Literal(Literal::String(name)));
26590                    args.push(val);
26591                    if matches!(self.peek(), Token::Comma) {
26592                        self.advance();
26593                    } else {
26594                        break;
26595                    }
26596                }
26597                if !matches!(self.peek(), Token::RParen) {
26598                    return Err(self.err(format!(
26599                        "expected ')' to close XMLFOREST, got {:?}",
26600                        self.peek()
26601                    )));
26602                }
26603                self.advance();
26604                return Ok(Expr::FunctionCall {
26605                    name: String::from("xmlforest"),
26606                    args,
26607                });
26608            }
26609            // SQL-standard `POSITION(sub IN str)` — lowers onto
26610            // strpos(str, sub). IN is the argument separator here,
26611            // so the needle parses with the IN-tail suppressed.
26612            if first.eq_ignore_ascii_case("position") && !matches!(self.peek(), Token::RParen) {
26613                let saved = self.suppress_in_tail;
26614                self.suppress_in_tail = true;
26615                let needle = self.parse_expr(0);
26616                self.suppress_in_tail = saved;
26617                let needle = needle?;
26618                if matches!(self.peek(), Token::In) {
26619                    self.advance();
26620                    let haystack = self.parse_expr(0)?;
26621                    if !matches!(self.peek(), Token::RParen) {
26622                        return Err(self.err(format!(
26623                            "expected ')' to close POSITION, got {:?}",
26624                            self.peek()
26625                        )));
26626                    }
26627                    self.advance();
26628                    return Ok(Expr::FunctionCall {
26629                        name: String::from("strpos"),
26630                        args: alloc::vec![haystack, needle],
26631                    });
26632                }
26633                // position(sub, str) comma form (incl. bytea) —
26634                // hand the parsed first arg to the generic list.
26635                args.push(needle);
26636                if matches!(self.peek(), Token::Comma) {
26637                    self.advance();
26638                }
26639            }
26640            // SQL-standard `TRIM([BOTH|LEADING|TRAILING] [chars]
26641            // FROM str)` — lowers onto btrim / ltrim / rtrim. The
26642            // plain comma forms TRIM(str) / TRIM(str, chars) keep
26643            // riding the generic argument list below.
26644            if first.eq_ignore_ascii_case("trim") {
26645                let mode = match self.peek() {
26646                    Token::Ident(k) if k.eq_ignore_ascii_case("both") => {
26647                        self.advance();
26648                        Some("btrim")
26649                    }
26650                    Token::Ident(k) if k.eq_ignore_ascii_case("leading") => {
26651                        self.advance();
26652                        Some("ltrim")
26653                    }
26654                    Token::Ident(k) if k.eq_ignore_ascii_case("trailing") => {
26655                        self.advance();
26656                        Some("rtrim")
26657                    }
26658                    _ => None,
26659                };
26660                if mode.is_some() || matches!(self.peek(), Token::From) {
26661                    // TRIM([mode] FROM str) — no strip-chars.
26662                    let chars = if matches!(self.peek(), Token::From) {
26663                        None
26664                    } else {
26665                        Some(self.parse_expr(0)?)
26666                    };
26667                    if !matches!(self.peek(), Token::From) {
26668                        return Err(self.err(format!(
26669                            "expected FROM in TRIM([BOTH|LEADING|TRAILING] [chars] FROM str), got {:?}",
26670                            self.peek()
26671                        )));
26672                    }
26673                    self.advance();
26674                    let target = self.parse_expr(0)?;
26675                    if !matches!(self.peek(), Token::RParen) {
26676                        return Err(
26677                            self.err(format!("expected ')' to close TRIM, got {:?}", self.peek()))
26678                        );
26679                    }
26680                    self.advance();
26681                    let mut trim_args = alloc::vec![target];
26682                    if let Some(c) = chars {
26683                        trim_args.push(c);
26684                    }
26685                    return Ok(Expr::FunctionCall {
26686                        name: String::from(mode.unwrap_or("btrim")),
26687                        args: trim_args,
26688                    });
26689                }
26690            }
26691            if !matches!(self.peek(), Token::RParen) {
26692                loop {
26693                    // v7.38 (read01, T14) — `argname => value` names this arg.
26694                    // v7.39 (read01 round 77) — `argname := value` is the same
26695                    // thing, and it is the spelling PG's own docs lead with. It
26696                    // was simply never lexed here, so every `f(x := 1)` died in
26697                    // the parser regardless of what `f` was.
26698                    let this_name = match (&self.tokens[self.pos], self.tokens.get(self.pos + 1)) {
26699                        (
26700                            Token::Ident(n) | Token::QuotedIdent(n),
26701                            Some(Token::FatArrow | Token::ColonEq),
26702                        ) => {
26703                            let name = n.clone();
26704                            self.advance(); // name
26705                            self.advance(); // => / :=
26706                            Some(name)
26707                        }
26708                        _ => None,
26709                    };
26710                    // v7.39 (read01 round 100) — `VARIADIC <array>` spreads an
26711                    // array's elements into a variadic call's trailing args
26712                    // (`concat_ws(',', VARIADIC ARRAY[…])`). VARIADIC isn't
26713                    // reserved, so it arrives as a bare ident before the arg.
26714                    let is_variadic = this_name.is_none()
26715                        && matches!(&self.tokens[self.pos], Token::Ident(s) if s.eq_ignore_ascii_case("variadic"));
26716                    if is_variadic {
26717                        self.advance();
26718                    }
26719                    let arg = self.parse_expr(0)?;
26720                    args.push(match &this_name {
26721                        // The callee's parameter names decide the slot, and a
26722                        // user function's live in the catalog. Carry the name
26723                        // to eval rather than guessing here.
26724                        Some(n) => Expr::NamedArg {
26725                            name: n.clone(),
26726                            expr: Box::new(arg),
26727                        },
26728                        None if is_variadic => Expr::Variadic(Box::new(arg)),
26729                        None => arg,
26730                    });
26731                    arg_names.push(this_name);
26732                    // v7.25 (round-17) — standard `CAST(expr AS type)`.
26733                    // The `::` cast already worked; this lowers the
26734                    // function form onto the same Expr::Cast node.
26735                    if first.eq_ignore_ascii_case("cast")
26736                        && args.len() == 1
26737                        && matches!(self.peek(), Token::As)
26738                    {
26739                        self.advance();
26740                        let target = self.parse_cast_target()?;
26741                        if !matches!(self.peek(), Token::RParen) {
26742                            return Err(self.err(format!(
26743                                "expected ')' to close CAST, got {:?}",
26744                                self.peek()
26745                            )));
26746                        }
26747                        self.advance();
26748                        return Ok(Expr::Cast {
26749                            expr: Box::new(args.pop().expect("one arg")),
26750                            target,
26751                        });
26752                    }
26753                    // v7.38 (read01 P6.-) — `normalize(text, FORM)` where FORM is
26754                    // a bare keyword NFC / NFD / NFKC / NFKD. PG parses these as
26755                    // keywords; SPG's lexer makes them plain idents (so they'd be
26756                    // read as column refs). Lower the keyword to the string form
26757                    // the evaluator already accepts.
26758                    if first.eq_ignore_ascii_case("normalize")
26759                        && args.len() == 1
26760                        && matches!(self.peek(), Token::Comma)
26761                    {
26762                        let form = match self.tokens.get(self.pos + 1) {
26763                            Some(Token::Ident(f) | Token::QuotedIdent(f)) => {
26764                                let up = f.to_ascii_uppercase();
26765                                matches!(up.as_str(), "NFC" | "NFD" | "NFKC" | "NFKD").then_some(up)
26766                            }
26767                            _ => None,
26768                        };
26769                        if let Some(up) = form {
26770                            self.advance(); // comma
26771                            self.advance(); // form keyword
26772                            args.push(Expr::Literal(Literal::String(up)));
26773                        }
26774                    }
26775                    // v7.37.7 C.1.8 — PG `substring(str FROM pos FOR len)` syntactic
26776                    // form. Desugars to the comma-list shape evaluator already
26777                    // handles. Triggered after the first arg when the function
26778                    // name is substring / substr and the next token is FROM
26779                    // (a reserved keyword in PG; SPG also reserves it).
26780                    // v7.39 (read01 regexp.c) — `substring(str SIMILAR pat
26781                    // ESCAPE esc)` (SQL:1999 three-part form) desugars to the
26782                    // internal __substring_similar(str, pat, esc) call.
26783                    if (first.eq_ignore_ascii_case("substring")
26784                        || first.eq_ignore_ascii_case("substr"))
26785                        && args.len() == 1
26786                        && matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("similar"))
26787                    {
26788                        self.advance(); // SIMILAR
26789                        let pattern = self.parse_expr(0)?;
26790                        if !matches!(self.peek(), Token::Ident(s) if s.eq_ignore_ascii_case("escape"))
26791                        {
26792                            return Err(self.err(format!(
26793                                "expected ESCAPE in substring(... SIMILAR ...), got {:?}",
26794                                self.peek()
26795                            )));
26796                        }
26797                        self.advance(); // ESCAPE
26798                        let esc = self.parse_expr(0)?;
26799                        if !matches!(self.peek(), Token::RParen) {
26800                            return Err(self.err(format!(
26801                                "expected ')' to close substring(... SIMILAR ...), got {:?}",
26802                                self.peek()
26803                            )));
26804                        }
26805                        self.advance();
26806                        args.push(pattern);
26807                        args.push(esc);
26808                        return Ok(Expr::FunctionCall {
26809                            name: "__substring_similar".to_string(),
26810                            args,
26811                        });
26812                    }
26813                    if (first.eq_ignore_ascii_case("substring")
26814                        || first.eq_ignore_ascii_case("substr"))
26815                        && args.len() == 1
26816                        && matches!(self.peek(), Token::From | Token::For)
26817                    {
26818                        // `substring(str FROM pos [FOR len])`, or the FOR-only
26819                        // `substring(str FOR len)` which PG treats as FROM 1.
26820                        if matches!(self.peek(), Token::From) {
26821                            self.advance();
26822                            let start = self.parse_expr(0)?;
26823                            args.push(start);
26824                        } else {
26825                            args.push(Expr::Literal(Literal::Integer(1)));
26826                        }
26827                        if matches!(self.peek(), Token::For) {
26828                            self.advance();
26829                            let length = self.parse_expr(0)?;
26830                            args.push(length);
26831                        }
26832                        if !matches!(self.peek(), Token::RParen) {
26833                            return Err(self.err(format!(
26834                                "expected ')' to close substring(... FROM ... [FOR ...]), got {:?}",
26835                                self.peek()
26836                            )));
26837                        }
26838                        self.advance();
26839                        return Ok(Expr::FunctionCall {
26840                            name: first.to_ascii_lowercase(),
26841                            args,
26842                        });
26843                    }
26844                    // PG `overlay(str PLACING repl FROM n [FOR len])`
26845                    // syntactic form. Desugars to the `overlay(str,
26846                    // repl, n[, len])` comma-list shape the evaluator
26847                    // already implements. `PLACING` is not a reserved
26848                    // token in SPG, so it arrives as a bare Ident.
26849                    if first.eq_ignore_ascii_case("overlay")
26850                        && args.len() == 1
26851                        && matches!(self.peek(), Token::Ident(kw) if kw == "placing")
26852                    {
26853                        self.advance(); // consume PLACING
26854                        args.push(self.parse_expr(0)?); // replacement
26855                        if !matches!(self.peek(), Token::From) {
26856                            return Err(self.err(format!(
26857                                "expected FROM in overlay(... PLACING ... FROM ...), got {:?}",
26858                                self.peek()
26859                            )));
26860                        }
26861                        self.advance();
26862                        args.push(self.parse_expr(0)?); // start position
26863                        if matches!(self.peek(), Token::For) {
26864                            self.advance();
26865                            args.push(self.parse_expr(0)?); // length
26866                        }
26867                        if !matches!(self.peek(), Token::RParen) {
26868                            return Err(self.err(format!(
26869                                "expected ')' to close overlay(... PLACING ... FROM ... [FOR ...]), got {:?}",
26870                                self.peek()
26871                            )));
26872                        }
26873                        self.advance();
26874                        return Ok(Expr::FunctionCall {
26875                            name: String::from("overlay"),
26876                            args,
26877                        });
26878                    }
26879                    // `TRIM(chars FROM str)` — the keyword-less
26880                    // spelling lands here after the chars parse
26881                    // (the keyword forms return earlier).
26882                    if first.eq_ignore_ascii_case("trim")
26883                        && args.len() == 1
26884                        && matches!(self.peek(), Token::From)
26885                    {
26886                        self.advance();
26887                        let target = self.parse_expr(0)?;
26888                        if !matches!(self.peek(), Token::RParen) {
26889                            return Err(self.err(format!(
26890                                "expected ')' to close TRIM(chars FROM str), got {:?}",
26891                                self.peek()
26892                            )));
26893                        }
26894                        self.advance();
26895                        let chars = args.pop().expect("one arg");
26896                        return Ok(Expr::FunctionCall {
26897                            name: String::from("btrim"),
26898                            args: alloc::vec![target, chars],
26899                        });
26900                    }
26901                    // v7.24 (round-16 A) — aggregate-internal
26902                    // ordering: `array_agg(x ORDER BY y DESC NULLS
26903                    // LAST)`. Keys close the argument list.
26904                    if matches!(self.peek(), Token::Order) {
26905                        self.advance();
26906                        if !self.peek_is_by() {
26907                            return Err(self.err(format!(
26908                                "expected BY after ORDER in aggregate args, got {:?}",
26909                                self.peek()
26910                            )));
26911                        }
26912                        self.advance();
26913                        loop {
26914                            // v7.39 (round 691) — save/restore, the discipline this parser
26915                            // already uses around `pending_sample_preds`, so a subquery inside
26916                            // a key neither inherits nor leaks the channel.
26917                            let saved_flag = core::mem::replace(&mut self.in_order_by_key, true);
26918                            let saved_coll = self.order_key_collation.take();
26919                            let parsed = self.parse_expr(0);
26920                            self.in_order_by_key = saved_flag;
26921                            let collation =
26922                                core::mem::replace(&mut self.order_key_collation, saved_coll);
26923                            let expr = parsed?;
26924                            let desc = if matches!(self.peek(), Token::Desc) {
26925                                self.advance();
26926                                true
26927                            } else if matches!(self.peek(), Token::Asc) {
26928                                self.advance();
26929                                false
26930                            } else {
26931                                false
26932                            };
26933                            let nulls_first = self.parse_optional_nulls_placement()?;
26934                            agg_order_by.push(OrderBy {
26935                                expr,
26936                                desc,
26937                                nulls_first,
26938                                collation,
26939                            });
26940                            if matches!(self.peek(), Token::Comma) {
26941                                self.advance();
26942                            } else {
26943                                break;
26944                            }
26945                        }
26946                        // v7.39 (round 354, M12) — `SEPARATOR '<s>'` may
26947                        // follow the ORDER BY inside GROUP_CONCAT.
26948                        if self.consume_group_concat_separator(&mut args)? {
26949                            saw_separator = true;
26950                        }
26951                        if !matches!(self.peek(), Token::RParen) {
26952                            return Err(self.err(format!(
26953                                "expected ')' after aggregate ORDER BY, got {:?}",
26954                                self.peek()
26955                            )));
26956                        }
26957                        break;
26958                    }
26959                    // v7.39 (round 354, M12) — …or directly after the
26960                    // arguments (`GROUP_CONCAT(t SEPARATOR '|')`). MySQL's
26961                    // own spelling of what PG passes as string_agg's second
26962                    // argument; it was a parse error, so every MySQL query
26963                    // that names its own separator failed outright.
26964                    if self.consume_group_concat_separator(&mut args)? {
26965                        saw_separator = true;
26966                        break;
26967                    }
26968                    match self.peek() {
26969                        Token::Comma => {
26970                            self.advance();
26971                        }
26972                        Token::RParen => break,
26973                        other => {
26974                            return Err(self.err(format!(
26975                                "expected ',' or ')' in function args, got {other:?}"
26976                            )));
26977                        }
26978                    }
26979                }
26980            }
26981            // v7.39 (round 354, M12) — MySQL's GROUP_CONCAT concatenates
26982            // its value arguments PER ROW: `GROUP_CONCAT(n, ':', t)` is
26983            // `3:c,1:a,…` (measured on MariaDB 11), NOT a second argument
26984            // meaning a separator — that is what the explicit SEPARATOR
26985            // tail is for. Fold them into one `concat(...)` so the
26986            // aggregate keeps its single value argument.
26987            if self.mysql_dialect && first.eq_ignore_ascii_case("group_concat") {
26988                let values = args.len() - usize::from(saw_separator);
26989                if values > 1 {
26990                    let sep_arg = if saw_separator { args.pop() } else { None };
26991                    let folded = Expr::FunctionCall {
26992                        name: "concat".to_string(),
26993                        args: core::mem::take(&mut args),
26994                    };
26995                    args.push(folded);
26996                    if let Some(sep) = sep_arg {
26997                        args.push(sep);
26998                    }
26999                }
27000            }
27001            self.advance(); // consume ')'
27002            // v7.39 (read01 round 77) — named arguments are NOT reordered here
27003            // any more. The parser has no catalog, so it could only ever resolve
27004            // the handful of `make_*` builtins whose parameter names were baked
27005            // into a table right here — every user function got
27006            // "does not support named arguments", though the catalog has been
27007            // storing its parameter names all along. Reordering happens in eval,
27008            // in one place, for builtins and user functions alike.
27009            // v7.32 (round-29) — ordered-set aggregate tail
27010            // `name(direct_args) WITHIN GROUP (ORDER BY …)`
27011            // (percentile_cont / percentile_disc / mode). The sort spec
27012            // lands in the same `order_by` slot a decorated aggregate
27013            // uses; the executor dispatches on the function name. WITHIN
27014            // GROUP and an intra-argument ORDER BY are mutually
27015            // exclusive (PG rejects both).
27016            let within_group_order = self.parse_within_group_clause()?;
27017            if !within_group_order.is_empty() && !agg_order_by.is_empty() {
27018                return Err(self.err(
27019                    "an aggregate may not carry both an in-argument ORDER BY and WITHIN GROUP"
27020                        .into(),
27021                ));
27022            }
27023            let within_group_seen = !within_group_order.is_empty();
27024            let agg_order_by = if within_group_order.is_empty() {
27025                agg_order_by
27026            } else {
27027                within_group_order
27028            };
27029            // v7.32 (round-29) — `name(args) FILTER (WHERE …)`.
27030            let filter = self.parse_filter_clause()?;
27031            // v4.12: window-function tail — `name(args) OVER (...)`.
27032            // Promotes the just-parsed FunctionCall into a
27033            // WindowFunction node carrying partition + order.
27034            // v6.4.2: also accepts `name(args) IGNORE NULLS OVER (...)`
27035            // / `RESPECT NULLS OVER (...)` between the closing paren
27036            // and `OVER`.
27037            let null_treatment = self.parse_null_treatment_modifier();
27038            if let Token::Ident(s) | Token::QuotedIdent(s) = self.peek()
27039                && s.eq_ignore_ascii_case("over")
27040            {
27041                self.advance();
27042                // v7.39 (round 230) — PG implements neither modifier for a
27043                // windowed call and says so (0A000). Both used to be parsed
27044                // and then silently dropped here, so `count(DISTINCT v)
27045                // OVER (…)` quietly answered the non-distinct count.
27046                if agg_distinct {
27047                    return Err(
27048                        self.err("DISTINCT is not implemented for window functions".to_string())
27049                    );
27050                }
27051                if !agg_order_by.is_empty() {
27052                    // PG separates the two shapes that land here: a
27053                    // WITHIN GROUP call is an ordered-set aggregate and gets
27054                    // its own message naming the aggregate; a plain
27055                    // `agg(x ORDER BY y)` gets the generic one.
27056                    let msg = if within_group_seen {
27057                        alloc::format!("OVER is not supported for ordered-set aggregate {first}")
27058                    } else {
27059                        "aggregate ORDER BY is not implemented for window functions".to_string()
27060                    };
27061                    return Err(self.err(msg));
27062                }
27063                let (partition_by, order_by, frame) = self.parse_over_clause()?;
27064                return Ok(Expr::WindowFunction {
27065                    name: first,
27066                    args,
27067                    partition_by,
27068                    order_by,
27069                    frame,
27070                    null_treatment,
27071                    filter,
27072                });
27073            }
27074            if !agg_order_by.is_empty() || agg_distinct || filter.is_some() {
27075                return Ok(Expr::AggregateOrdered {
27076                    call: Box::new(Expr::FunctionCall { name: first, args }),
27077                    order_by: agg_order_by,
27078                    distinct: agg_distinct,
27079                    filter,
27080                });
27081            }
27082            // v7.39 (round 522) — PG declares `date_add` / `date_subtract`
27083            // over TIMESTAMPTZ and has no timestamp overload, so a
27084            // timestamp argument is coerced on the way in and the answer
27085            // is timestamptz — measured: `pg_typeof(date_add(TIMESTAMP
27086            // '2020-01-01', INTERVAL '1 hour'))` is `timestamp with time
27087            // zone`. SPG answered `timestamp without time zone`, dropping
27088            // the offset from every rendering.
27089            //
27090            // Writing the coercion PG performs makes the existing
27091            // argument-driven typing (the one `date_trunc` uses) reach the
27092            // right answer, rather than teaching the type layer a second
27093            // rule. MySQL's DATE_ADD is a different function that returns
27094            // DATE or DATETIME, so this is PG-dialect only.
27095            //
27096            // Out-of-line because this sits on the RECURSIVE descent
27097            // frame: an inline block with locals here costs every nesting
27098            // level, and the suite's deep-nesting sentinel overflowed the
27099            // 512 KiB parser stack the moment one was added (round 430's
27100            // lesson, in the same shape).
27101            if !self.mysql_dialect {
27102                lift_date_add_arg_to_timestamptz(&first, &mut args);
27103            }
27104            return Ok(Expr::FunctionCall { name: first, args });
27105        }
27106        // v7.9.20 — SQL-standard parenless keyword expressions
27107        // (PG treats these as functions called without parens).
27108        // Resolve to a synthetic FunctionCall so the engine's
27109        // eval path reuses the existing function-call routing.
27110        // mailrs G3.
27111        let lc = first.to_ascii_lowercase();
27112        if matches!(
27113            lc.as_str(),
27114            "current_date"
27115                | "current_time"
27116                | "current_timestamp"
27117                | "localtimestamp"
27118                | "localtime"
27119                // v7.37.17 (17.6 siblings) — session-identity SQL-
27120                // standard parenless keywords. current_user /
27121                // session_user / user were already caught by the
27122                // pgwire canned-response shortcut but bare-select
27123                // in the embedded engine went through Expr::Column
27124                // and errored. Adding them here so the parser
27125                // resolves to a synthetic FunctionCall that reuses
27126                // the existing eval/functions.rs dispatch.
27127                | "current_user"
27128                | "session_user"
27129                | "current_role"
27130                | "current_catalog"
27131                | "current_schema"
27132                | "current_database"
27133                // v7.39 (read01 round 51) — PG 16's system_user is parenless too.
27134                | "system_user"
27135        ) {
27136            return Ok(Expr::FunctionCall {
27137                name: lc,
27138                args: Vec::new(),
27139            });
27140        }
27141        Ok(Expr::Column(ColumnName {
27142            qualifier: None,
27143            name: first,
27144        }))
27145    }
27146}
27147
27148/// v7.39 (round 522) — write the coercion PG's `date_add` /
27149/// `date_subtract` signature performs.
27150///
27151/// PG declares both over TIMESTAMPTZ and has no timestamp overload, so a
27152/// timestamp argument is cast on the way in and the answer is
27153/// timestamptz — measured: `pg_typeof(date_add(TIMESTAMP '2020-01-01',
27154/// INTERVAL '1 hour'))` is `timestamp with time zone`. SPG answered
27155/// `timestamp without time zone`, dropping the offset from every
27156/// rendering of the result.
27157///
27158/// Writing the cast the signature implies lets the existing
27159/// argument-driven typing (the one `date_trunc` uses) reach the right
27160/// answer instead of teaching the type layer a second rule. MySQL's
27161/// DATE_ADD is a different function returning DATE or DATETIME, so the
27162/// caller applies this in PG dialect only.
27163///
27164/// A free function, and not a block at the call site, because the caller
27165/// is on the recursive-descent frame chain.
27166#[inline(never)]
27167fn lift_date_add_arg_to_timestamptz(name: &str, args: &mut alloc::vec::Vec<Expr>) {
27168    if args.len() != 2
27169        || !(name.eq_ignore_ascii_case("date_add") || name.eq_ignore_ascii_case("date_subtract"))
27170    {
27171        return;
27172    }
27173    let base = args.remove(0);
27174    args.insert(
27175        0,
27176        Expr::Cast {
27177            expr: Box::new(base),
27178            target: CastTarget::Timestamptz,
27179        },
27180    );
27181}
27182
27183/// v6.8.2 — walk an expression tree and return the first column
27184/// reference's bare name. Used by `parse_create_index_stmt_after_create`
27185/// to derive `CreateIndexStatement.column` from an expression
27186/// key (so downstream planner code resolving a primary column
27187/// position keeps working with expression indexes). Returns
27188/// `None` when the expression has no column ref at all — caller
27189/// surfaces that as a parse error.
27190fn extract_first_column(expr: &Expr) -> Option<String> {
27191    match expr {
27192        Expr::Column(cn) => Some(cn.name.clone()),
27193        Expr::FunctionCall { args, .. } => args.iter().find_map(extract_first_column),
27194        Expr::Binary { lhs, rhs, .. } => {
27195            extract_first_column(lhs).or_else(|| extract_first_column(rhs))
27196        }
27197        Expr::Unary { expr: e, .. } => extract_first_column(e),
27198        // v7.39 (read01 round 93) — a cast wraps its operand: a common
27199        // expression-index key is `lower(col::text)`, where the column
27200        // sits under the `::text` cast inside the function arg. Without
27201        // descending here the key was rejected as "references no column".
27202        Expr::Cast { expr: e, .. } => extract_first_column(e),
27203        // v7.39.2 — and a COLLATE wraps its operand the same way.
27204        // `CREATE INDEX rc ON t (c COLLATE "C" DESC)` stopped naming a
27205        // column the moment the clause became a node instead of being
27206        // absorbed, and the key was rejected as referencing none. This
27207        // is the shape the wildcard below silently produces, which is
27208        // why it is spelled out.
27209        Expr::Collate { expr: e, .. } => extract_first_column(e),
27210        _ => None,
27211    }
27212}
27213
27214fn maybe_not(expr: Expr, negated: bool) -> Expr {
27215    if negated {
27216        Expr::Unary {
27217            op: UnOp::Not,
27218            expr: Box::new(expr),
27219        }
27220    } else {
27221        expr
27222    }
27223}
27224
27225/// v7.39 (round 353, M9/M10) — three operator TOKENS mean different
27226/// things in the two dialects, and SPG read all three PG's way:
27227///
27228/// | token | PG (and SPG) | MySQL, measured |
27229/// |---|---|---|
27230/// | `\|\|` | string concatenation | **OR** — `1 \|\| 0` is 1, not '10' |
27231/// | `&&` | inet / array overlap | **AND** |
27232/// | `<=>` | pgvector cosine distance | **NULL-safe equal** |
27233///
27234/// `1 || 0` answering the string '10' on a MySQL session is a wrong
27235/// answer with no error, which is why they are routed here rather than
27236/// left to the shared table.
27237impl Parser {
27238    fn binop_here(&self, tok: &Token) -> Option<(BinOp, u8)> {
27239        if self.mysql_dialect {
27240            // v7.39 (round 353, M9) — `DIV` is MySQL's truncating integer
27241            // division (`5 DIV 2` is 2, `-7 DIV 2` is -3 — toward zero —
27242            // and `5 DIV 0` is NULL). It is a plain ident to the lexer.
27243            if let Token::Ident(w) = tok
27244                && w.eq_ignore_ascii_case("div")
27245            {
27246                return Some((BinOp::IntDiv, 8));
27247            }
27248            // v7.39 (round 394) — `MOD` is MySQL's modulo operator, a synonym
27249            // for `%` (`10 MOD 3` is 1, `5.5 MOD 2` is 1.5). A plain ident to
27250            // the lexer; the `MOD(x, y)` function form is unaffected (MOD
27251            // there sits in operand position, not infix).
27252            if let Token::Ident(w) = tok
27253                && w.eq_ignore_ascii_case("mod")
27254            {
27255                return Some((BinOp::Mod, 8));
27256            }
27257            // v7.39 (round 407) — `XOR` is MySQL's logical exclusive-or, a
27258            // plain ident to the lexer. Its precedence sits between OR (1)
27259            // and AND (3) — hence rung 2, the slot freed by moving AND up.
27260            if let Token::Ident(w) = tok
27261                && w.eq_ignore_ascii_case("xor")
27262            {
27263                return Some((BinOp::LogicalXor, 2));
27264            }
27265            match tok {
27266                Token::Concat => return Some((BinOp::Or, 1)),
27267                // MySQL's `&&` is logical AND, sharing AND's rung (3).
27268                Token::InetOverlap => return Some((BinOp::And, 3)),
27269                // MySQL's `<=>` is NULL-safe equal, at the comparison rung (5).
27270                Token::CosineDistance => return Some((BinOp::IsNotDistinctFrom, 5)),
27271                _ => {}
27272            }
27273        }
27274        binop_from(tok)
27275    }
27276}
27277
27278// v7.39 (round 407) — precedence ladder. To open a rung for MySQL's `XOR`
27279// (which sits strictly between OR and AND), every level from AND upward was
27280// shifted +1: the ladder is now OR=1, XOR=2, AND=3, IS=4, comparison=5,
27281// distance=6, additive/concat/bitwise=7, multiplicative/JSON=8, prefix=9.
27282// XOR only exists in the MySQL dialect (binop_here); PG never sees it, and
27283// the *relative* order of every PG operator is unchanged by the shift.
27284fn binop_from(tok: &Token) -> Option<(BinOp, u8)> {
27285    let pair = match tok {
27286        Token::Or => (BinOp::Or, 1),
27287        Token::And => (BinOp::And, 3),
27288        Token::Eq => (BinOp::Eq, 5),
27289        Token::NotEq => (BinOp::NotEq, 5),
27290        Token::Lt => (BinOp::Lt, 5),
27291        Token::LtEq => (BinOp::LtEq, 5),
27292        Token::Gt => (BinOp::Gt, 5),
27293        Token::GtEq => (BinOp::GtEq, 5),
27294        // pgvector distance ops all sit on the same rung — tighter than
27295        // comparisons (5) so `col <-> v < threshold` parses correctly.
27296        Token::L2Distance => (BinOp::L2Distance, 6),
27297        // v7.39 (read01 geo_ops.c) — geometric predicates ride the
27298        // comparison rung.
27299        Token::GeomParallel => (BinOp::GeomParallel, 5),
27300        // v7.39 (read01 rangetypes.c) — range `&<` / `&>` on the
27301        // comparison rung.
27302        Token::OverLeft => (BinOp::OverLeft, 5),
27303        Token::OverRight => (BinOp::OverRight, 5),
27304        Token::GeomPerp => (BinOp::GeomPerp, 5),
27305        Token::GeomSameAs => (BinOp::GeomSameAs, 5),
27306        Token::ClosestPoint => (BinOp::ClosestPoint, 6),
27307        Token::GeomHoriz => (BinOp::GeomHoriz, 5),
27308        Token::InnerProduct => (BinOp::InnerProduct, 6),
27309        Token::CosineDistance => (BinOp::CosineDistance, 6),
27310        Token::Plus => (BinOp::Add, 7),
27311        Token::Minus => (BinOp::Sub, 7),
27312        // v7.39 (round 760, F31-B1) — the generic-operator rung. PG
27313        // binds every "other" operator (`||`, `|`, `&`, `#`, the
27314        // pgvector distances above) BETWEEN additive (7) and the
27315        // comparisons (5): `'a' || 1 + 1` is `'a' || 2` → `a2`,
27316        // `a & b + 1` is `a & (b + 1)`, and `flags & $1 = 0` stays
27317        // `(flags & $1) = 0`. They shared rung 7 with `+ -` since v1
27318        // ("matches PG conceptually" — the round-753 audit measured it
27319        // false; the old rung errored on `'a' || 1 + 1` with
27320        // `text + integer`). Same-level chains left-fold, as PG does.
27321        Token::Concat => (BinOp::Concat, 6),
27322        Token::Pipe => (BinOp::BitOr, 6),
27323        Token::Amp => (BinOp::BitAnd, 6),
27324        Token::Star => (BinOp::Mul, 8),
27325        Token::Slash => (BinOp::Div, 8),
27326        Token::Percent => (BinOp::Mod, 8),
27327        // v4.14: JSON path ops bind tighter than comparisons (5)
27328        // and additive (7) so `doc->'k' = 'v'` parses correctly.
27329        // Same rung as the multiplicative ops.
27330        Token::JsonGet => (BinOp::JsonGet, 8),
27331        Token::JsonGetText => (BinOp::JsonGetText, 8),
27332        Token::JsonGetPath => (BinOp::JsonGetPath, 8),
27333        Token::JsonGetPathText => (BinOp::JsonGetPathText, 8),
27334        Token::JsonContains => (BinOp::JsonContains, 8),
27335        Token::JsonPathExists => (BinOp::JsonPathExists, 8),
27336        Token::JsonContainedBy => (BinOp::JsonContainedBy, 8),
27337        Token::JsonKeyExists => (BinOp::JsonKeyExists, 8),
27338        Token::JsonKeysAny => (BinOp::JsonKeysAny, 8),
27339        Token::JsonKeysAll => (BinOp::JsonKeysAll, 8),
27340        Token::JsonDeletePath => (BinOp::JsonDeletePath, 8),
27341        // v7.12.2 — `@@` binds at the comparison rung (looser than
27342        // arithmetic, tighter than AND / OR). PG places `@@` at
27343        // the same precedence as `=` / `<`, so we follow.
27344        Token::TsMatch => (BinOp::TsMatch, 5),
27345        // v7.17.0 Phase 3.P0-47 — PG INET / CIDR containment + overlap.
27346        // PG places these at the comparison rung (same level as `=`),
27347        // so we follow.
27348        Token::InetContainedBy => (BinOp::InetContainedBy, 5),
27349        Token::InetContainedByEq => (BinOp::InetContainedByEq, 5),
27350        Token::InetContains => (BinOp::InetContains, 5),
27351        Token::InetContainsEq => (BinOp::InetContainsEq, 5),
27352        Token::InetOverlap => (BinOp::InetOverlap, 5),
27353        // v7.39 (round 508) — the geometric and pattern-order predicates
27354        // ride the comparison rung, as every other predicate does.
27355        Token::Intersects => (BinOp::Intersects, 5),
27356        Token::IsBelow => (BinOp::IsBelow, 5),
27357        Token::IsAbove => (BinOp::IsAbove, 5),
27358        Token::PatternLt => (BinOp::PatternLt, 5),
27359        Token::PatternLtEq => (BinOp::PatternLtEq, 5),
27360        Token::PatternGt => (BinOp::PatternGt, 5),
27361        Token::PatternGtEq => (BinOp::PatternGtEq, 5),
27362        // `@@@` is the old spelling of `@@` and means exactly it.
27363        Token::TsMatchOld => (BinOp::TsMatch, 5),
27364        _ => return None,
27365    };
27366    Some(pair)
27367}
27368
27369#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27370// `as f32` here is intentional: vector elements widen / narrow into f32 on
27371// purpose. i64 → f32 loses precision past 2^24, f64 → f32 loses precision
27372// past ~15 decimal digits — both are acceptable for a fixed-precision
27373// pgvector column.
27374/// v7.17.0 Phase 1.3 — words that would otherwise be eaten as an
27375/// implicit table alias and break trailing clauses. WITH lands
27376/// here so `… FROM t WITH NO DATA` doesn't consume WITH as the
27377/// alias for `t`; same for ON / WHERE / HAVING / GROUP / ORDER /
27378/// LIMIT / OFFSET / UNION / EXCEPT / INTERSECT / RETURNING / SET
27379/// / VALUES / FOR / LATERAL — all of which would otherwise be
27380/// silently swallowed by `parse_optional_alias`.
27381fn is_alias_stopword(s: &str) -> bool {
27382    matches!(
27383        s.to_ascii_lowercase().as_str(),
27384        "with"
27385            | "on"
27386            | "where"
27387            | "having"
27388            | "group"
27389            | "order"
27390            | "limit"
27391            | "offset"
27392            | "union"
27393            | "except"
27394            | "intersect"
27395            | "returning"
27396            | "set"
27397            | "values"
27398            | "for"
27399            | "window"
27400            | "tablesample"
27401            | "lateral"
27402            | "left"
27403            | "right"
27404            | "inner"
27405            | "outer"
27406            | "full"
27407            | "cross"
27408            | "join"
27409            | "natural"
27410            | "using"
27411            | "fetch"
27412    )
27413}
27414
27415fn extract_numeric_literal(e: &Expr) -> Option<f32> {
27416    match e {
27417        Expr::Literal(Literal::Integer(n)) => Some(*n as f32),
27418        Expr::Literal(Literal::Float(x)) => Some(*x as f32),
27419        // v7.38 (read01) — a dotted literal is now NUMERIC, so a vector element
27420        // like `2.5` arrives as Literal::Numeric; widen it into f32. (`no_std`,
27421        // so scale the divisor by hand instead of `f32::powi`.)
27422        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
27423            let mut div = 1.0f32;
27424            for _ in 0..*scale {
27425                div *= 10.0;
27426            }
27427            Some(*unscaled as f32 / div)
27428        }
27429        Expr::Unary {
27430            op: UnOp::Neg,
27431            expr,
27432        } => extract_numeric_literal(expr).map(|x| -x),
27433        _ => None,
27434    }
27435}
27436
27437/// Parse the text inside `INTERVAL '...'` into `(months, micros)`. Accepts
27438/// one or more `<n> <unit>` pairs separated by whitespace. `<n>` may be
27439/// negative. Returns `None` if any pair fails to parse or no pair is found.
27440///
27441/// Recognised units (case-insensitive, optional trailing `s`):
27442/// `microsecond`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
27443/// `month`, `year`. `week` widens to 7 days; `year` widens to 12 months.
27444/// v7.37.5 β — returns `(months, days, micros)`. `days` is preserved
27445/// as its own dimension so `INTERVAL '1 day'` ≠ `INTERVAL '24 hours'`
27446/// (PG-canonical: DST and month-boundary semantics depend on this).
27447/// `week` rolls into `days` (× 7). Sub-day units flow into `micros`.
27448/// ISO 8601 duration input for INTERVAL: `P1Y2M3DT4H5M6S`. Before the `T`,
27449/// `M` is months; after it, `M` is minutes. Returns `(months, days, micros)`.
27450#[allow(clippy::cast_possible_truncation)]
27451fn parse_iso8601_interval(rest: &str) -> Option<(i32, i32, i64)> {
27452    let mut months: i64 = 0;
27453    let mut days: i64 = 0;
27454    let mut micros: i64 = 0;
27455    let mut in_time = false;
27456    let mut num = alloc::string::String::new();
27457    for ch in rest.chars() {
27458        if ch.is_ascii_digit() || ch == '.' || ch == '-' || ch == '+' {
27459            num.push(ch);
27460            continue;
27461        }
27462        if ch == 'T' || ch == 't' {
27463            if !num.is_empty() {
27464                return None;
27465            }
27466            in_time = true;
27467            continue;
27468        }
27469        let n: f64 = num.parse().ok()?;
27470        num.clear();
27471        match (ch, in_time) {
27472            ('Y' | 'y', false) => months += (n * 12.0) as i64,
27473            ('M', false) => months += n as i64,
27474            ('W' | 'w', false) => days += (n * 7.0) as i64,
27475            ('D' | 'd', false) => days += n as i64,
27476            ('H' | 'h', true) => micros += (n * 3_600_000_000.0) as i64,
27477            ('M', true) => micros += (n * 60_000_000.0) as i64,
27478            ('S' | 's', true) => micros += (n * 1_000_000.0) as i64,
27479            _ => return None,
27480        }
27481    }
27482    if !num.is_empty() {
27483        return None;
27484    }
27485    Some((
27486        i32::try_from(months).ok()?,
27487        i32::try_from(days).ok()?,
27488        micros,
27489    ))
27490}
27491
27492/// PG year-month shorthand for INTERVAL: `1-2` = 1 year 2 mons (an optional
27493/// leading `-` negates the whole value). Rejects date-like strings.
27494fn parse_year_month_interval(s: &str) -> Option<(i32, i32, i64)> {
27495    let (neg, body) = match s.strip_prefix('-') {
27496        Some(b) => (true, b),
27497        None => (false, s),
27498    };
27499    let (y, m) = body.split_once('-')?;
27500    let years: i32 = y.parse().ok()?;
27501    let mons: i32 = m.parse().ok()?;
27502    if years < 0 || mons < 0 {
27503        return None;
27504    }
27505    let total = years.checked_mul(12)?.checked_add(mons)?;
27506    Some((if neg { -total } else { total }, 0, 0))
27507}
27508
27509/// Parse a clock-time interval token `HH:MM[:SS[.ffffff]]` (optionally signed)
27510/// into microseconds. Used for the `3 days 14:30:45` / bare `14:30:45` forms.
27511fn parse_interval_clock(tok: &str) -> Option<i64> {
27512    let (neg, body) = match tok.strip_prefix('-') {
27513        Some(r) => (true, r),
27514        None => (false, tok.strip_prefix('+').unwrap_or(tok)),
27515    };
27516    let mut it = body.split(':');
27517    let h: i64 = it.next()?.parse().ok()?;
27518    let m: i64 = it.next()?.parse().ok()?;
27519    let s_tok = it.next().unwrap_or("0");
27520    if it.next().is_some() {
27521        return None;
27522    }
27523    let sec_us: i64 = if let Some((sec, frac)) = s_tok.split_once('.') {
27524        let sec: i64 = sec.parse().ok()?;
27525        let mut f = alloc::string::String::from(frac);
27526        while f.len() < 6 {
27527            f.push('0');
27528        }
27529        f.truncate(6);
27530        let fus: i64 = f.parse().ok()?;
27531        sec.checked_mul(1_000_000)?.checked_add(fus)?
27532    } else {
27533        s_tok.parse::<i64>().ok()?.checked_mul(1_000_000)?
27534    };
27535    let total = h
27536        .checked_mul(3_600_000_000)?
27537        .checked_add(m.checked_mul(60_000_000)?)?
27538        .checked_add(sec_us)?;
27539    Some(if neg { -total } else { total })
27540}
27541
27542/// v7.39 (read01 round 77) — one canonical name per interval unit, covering
27543/// every spelling PG accepts (measured against live PG18.4, not guessed):
27544/// `min` / `mins` / `m` are minutes, `mon` / `mons` are months, `y` is years.
27545/// Before this, the unit table matched long names only, with an ad-hoc
27546/// `strip_suffix('s')` in front of it — so `'15 min'` (and `hrs`, `secs`,
27547/// `yrs`, every abbreviation anyone actually types) was "cannot parse as
27548/// INTERVAL", and it had also grown arms for the debris that stripping leaves
27549/// behind (`centurie`, `millenniu`). Two parallel unit matches (integer and
27550/// fractional) both read from this one table now.
27551fn canonical_interval_unit(raw: &str) -> Option<&'static str> {
27552    let u = raw.to_ascii_lowercase();
27553    Some(match u.as_str() {
27554        "microsecond" | "microseconds" | "us" | "usec" | "usecs" | "usecond" | "useconds" => {
27555            "microsecond"
27556        }
27557        "millisecond" | "milliseconds" | "ms" | "msec" | "msecs" | "msecond" | "mseconds" => {
27558            "millisecond"
27559        }
27560        "second" | "seconds" | "sec" | "secs" | "s" => "second",
27561        "minute" | "minutes" | "min" | "mins" | "m" => "minute",
27562        "hour" | "hours" | "hr" | "hrs" | "h" => "hour",
27563        "day" | "days" | "d" => "day",
27564        "week" | "weeks" | "w" => "week",
27565        "month" | "months" | "mon" | "mons" => "month",
27566        "year" | "years" | "yr" | "yrs" | "y" => "year",
27567        "decade" | "decades" | "dec" | "decs" => "decade",
27568        "century" | "centuries" | "cent" | "c" => "century",
27569        "millennium" | "millenniums" | "millennia" | "mil" | "mils" => "millennium",
27570        _ => return None,
27571    })
27572}
27573
27574/// v7.39 (read01 round 102) — the six SQL-standard interval fields that can
27575/// qualify an `INTERVAL '…' <FIELD> [TO <FIELD>]` literal.
27576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27577pub(crate) enum IntervalField {
27578    Year,
27579    Month,
27580    Day,
27581    Hour,
27582    Minute,
27583    Second,
27584}
27585
27586/// Recognise an interval field keyword (bare ident, case-insensitive). Plural
27587/// spellings aren't standard for the qualifier position, so only the singular
27588/// forms are accepted.
27589/// v7.39 (round 350, M7) — MySQL's interval units, measured against
27590/// MariaDB 11. QUARTER is three months and WEEK seven days; MICROSECOND
27591/// is the finest. (The compound spellings — `DAY_HOUR` and friends, which
27592/// take a `'1 2'` style literal — are not read here; they stay a parse
27593/// error rather than being silently misread.)
27594/// v7.39 (round 430) — lower a `@name` / `@@name` reference to its call.
27595///
27596/// ONE `@` is a MySQL USER variable: its own per-session namespace, nothing
27597/// to do with a `@@` engine setting, and an unset one reads NULL rather
27598/// than raising. (The parser used to strip every `@`, so `@x` and `@@x`
27599/// were the same node and `SELECT @x` answered "Unknown system variable".)
27600/// For `@@`, the `session.` / `global.` scope is KEPT: a global read must
27601/// not see a session override — measured, after `SET autocommit=0`,
27602/// `@@global.autocommit` is still 1.
27603///
27604/// Out-of-line and NOT a method: `parse_atom` is the giant recursive frame
27605/// the parser's nesting budget is tuned against, and building these
27606/// `String` + `Vec` locals inside it overflowed the guard's stack (the same
27607/// wall `parse_left_right_atom` and friends were factored out for).
27608#[inline(never)]
27609fn variable_ref_atom(raw: &str) -> Expr {
27610    let user_var = !raw.starts_with("@@");
27611    let bare = raw.trim_start_matches('@').to_ascii_lowercase();
27612    Expr::FunctionCall {
27613        name: String::from(if user_var {
27614            "__spg_user_var"
27615        } else {
27616            "__spg_session_var"
27617        }),
27618        args: alloc::vec![Expr::Literal(Literal::String(bare))],
27619    }
27620}
27621
27622fn mysql_interval_unit(tok: &Token) -> Option<&'static str> {
27623    let Token::Ident(s) = tok else { return None };
27624    Some(match () {
27625        () if s.eq_ignore_ascii_case("microsecond") => "microsecond",
27626        () if s.eq_ignore_ascii_case("second") => "second",
27627        () if s.eq_ignore_ascii_case("minute") => "minute",
27628        () if s.eq_ignore_ascii_case("hour") => "hour",
27629        () if s.eq_ignore_ascii_case("day") => "day",
27630        () if s.eq_ignore_ascii_case("week") => "week",
27631        () if s.eq_ignore_ascii_case("month") => "month",
27632        () if s.eq_ignore_ascii_case("quarter") => "quarter",
27633        () if s.eq_ignore_ascii_case("year") => "year",
27634        () => return None,
27635    })
27636}
27637
27638/// v7.39 (round 422) — lower `INTERVAL <expr> <unit>` onto the existing
27639/// `make_interval(years, months, weeks, days, hours, mins, secs)` builtin,
27640/// which constructs the value at run time. Only the slot the unit names
27641/// carries the quantity; QUARTER and MICROSECOND scale it into the nearest
27642/// slot the builtin has (months and fractional seconds respectively).
27643fn make_interval_call(qty: Expr, unit: &str) -> Expr {
27644    let zero = || Expr::Literal(Literal::Integer(0));
27645    let scaled = |op: crate::ast::BinOp, by: Expr| Expr::Binary {
27646        lhs: alloc::boxed::Box::new(qty.clone()),
27647        op,
27648        rhs: alloc::boxed::Box::new(by),
27649    };
27650    // (years, months, weeks, days, hours, mins, secs)
27651    let mut args = alloc::vec![zero(), zero(), zero(), zero(), zero(), zero(), zero()];
27652    match unit {
27653        "year" => args[0] = qty,
27654        "quarter" => {
27655            args[1] = scaled(crate::ast::BinOp::Mul, Expr::Literal(Literal::Integer(3)));
27656        }
27657        "month" => args[1] = qty,
27658        "week" => args[2] = qty,
27659        "day" => args[3] = qty,
27660        "hour" => args[4] = qty,
27661        "minute" => args[5] = qty,
27662        "second" => args[6] = qty,
27663        // The builtin's seconds slot takes a fraction, so microseconds ride
27664        // it scaled down; the divisor is a NUMERIC literal so the division
27665        // stays exact rather than going through a float.
27666        "microsecond" => {
27667            args[6] = scaled(
27668                crate::ast::BinOp::Div,
27669                Expr::Literal(Literal::Numeric {
27670                    unscaled: 1_000_000,
27671                    scale: 0,
27672                }),
27673            );
27674        }
27675        _ => args[3] = qty,
27676    }
27677    Expr::FunctionCall {
27678        name: alloc::string::String::from("make_interval"),
27679        args,
27680    }
27681}
27682
27683/// `(count, unit)` → `(months, days, micros)`.
27684fn scale_mysql_interval(count: &str, unit: &str) -> Option<(i32, i32, i64)> {
27685    let n: i64 = count.trim().parse().ok()?;
27686    Some(match unit {
27687        "microsecond" => (0, 0, n),
27688        "second" => (0, 0, n.checked_mul(1_000_000)?),
27689        "minute" => (0, 0, n.checked_mul(60_000_000)?),
27690        "hour" => (0, 0, n.checked_mul(3_600_000_000)?),
27691        "day" => (0, i32::try_from(n).ok()?, 0),
27692        "week" => (0, i32::try_from(n.checked_mul(7)?).ok()?, 0),
27693        "month" => (i32::try_from(n).ok()?, 0, 0),
27694        "quarter" => (i32::try_from(n.checked_mul(3)?).ok()?, 0, 0),
27695        "year" => (i32::try_from(n.checked_mul(12)?).ok()?, 0, 0),
27696        _ => return None,
27697    })
27698}
27699
27700fn interval_field_of(tok: &Token) -> Option<IntervalField> {
27701    let Token::Ident(s) = tok else { return None };
27702    Some(match () {
27703        () if s.eq_ignore_ascii_case("year") => IntervalField::Year,
27704        () if s.eq_ignore_ascii_case("month") => IntervalField::Month,
27705        () if s.eq_ignore_ascii_case("day") => IntervalField::Day,
27706        () if s.eq_ignore_ascii_case("hour") => IntervalField::Hour,
27707        () if s.eq_ignore_ascii_case("minute") => IntervalField::Minute,
27708        () if s.eq_ignore_ascii_case("second") => IntervalField::Second,
27709        () => return None,
27710    })
27711}
27712
27713/// v7.39 (read01 round 102) — interpret an interval literal under a field
27714/// qualifier. Returns `(months, days, micros)`.
27715///
27716/// * A single field applied to a bare number sets which unit the number means,
27717///   truncated to that field's precision (`INTERVAL '1.5' HOUR` → `01:00:00`);
27718///   SECOND keeps its fraction (`'90.5' SECOND` → `00:01:30.5`).
27719/// * `YEAR TO MONTH` reads the `Y-M` form (`'1-6'` → 1 year 6 months).
27720/// * Every other range, and any literal a single field can't read as a plain
27721///   number (`'2 days' DAY`), falls back to the unqualified parse — SPG's
27722///   interval-text parser already reads the `D H:MM:SS` / `H:MM` forms exactly
27723///   like PG, and the qualifier there only bounds precision.
27724fn interpret_qualified_interval(
27725    text: &str,
27726    (f1, f2): (IntervalField, Option<IntervalField>),
27727) -> Option<(i32, i32, i64)> {
27728    if let Some(f2) = f2 {
27729        if f1 == IntervalField::Year && f2 == IntervalField::Month {
27730            if let Some(m) = parse_year_month_literal(text) {
27731                return Some((m, 0, 0));
27732            }
27733        }
27734        return parse_interval_text(text);
27735    }
27736    // Single field: reinterpret a bare number; otherwise the default parse.
27737    let trimmed = text.trim();
27738    if let Ok(val) = trimmed.parse::<f64>() {
27739        // no_std: f64 has no trunc/round; cast toward zero + round-half-away.
27740        #[allow(clippy::cast_possible_truncation)]
27741        let whole = val as i64;
27742        #[allow(clippy::cast_possible_truncation)]
27743        let secs_micros = {
27744            let m = val * 1_000_000.0;
27745            if m >= 0.0 {
27746                (m + 0.5) as i64
27747            } else {
27748                (m - 0.5) as i64
27749            }
27750        };
27751        return Some(match f1 {
27752            IntervalField::Year => (i32::try_from(whole).ok()?.checked_mul(12)?, 0, 0),
27753            IntervalField::Month => (i32::try_from(whole).ok()?, 0, 0),
27754            IntervalField::Day => (0, i32::try_from(whole).ok()?, 0),
27755            IntervalField::Hour => (0, 0, whole.checked_mul(3_600_000_000)?),
27756            IntervalField::Minute => (0, 0, whole.checked_mul(60_000_000)?),
27757            IntervalField::Second => (0, 0, secs_micros),
27758        });
27759    }
27760    parse_interval_text(text)
27761}
27762
27763/// Parse the `Y-M` (optionally signed) year-to-month literal into total months.
27764fn parse_year_month_literal(text: &str) -> Option<i32> {
27765    let t = text.trim();
27766    let (neg, body) = match t.strip_prefix('-') {
27767        Some(r) => (true, r),
27768        None => (false, t.strip_prefix('+').unwrap_or(t)),
27769    };
27770    let mut it = body.split('-');
27771    let years: i32 = it.next()?.trim().parse().ok()?;
27772    let months: i32 = match it.next() {
27773        Some(m) => m.trim().parse().ok()?,
27774        None => 0,
27775    };
27776    if it.next().is_some() {
27777        return None;
27778    }
27779    let total = years.checked_mul(12)?.checked_add(months)?;
27780    Some(if neg { -total } else { total })
27781}
27782
27783pub fn parse_interval_text(s: &str) -> Option<(i32, i32, i64)> {
27784    // v7.38.19 — the two infinities, answered as the three extreme
27785    // fields PostgreSQL itself puts on the wire for them:
27786    //
27787    //   COPY (SELECT 'infinity'::interval) TO STDOUT (FORMAT binary)
27788    //     … 7fffffffffffffff 7fffffff 7fffffff
27789    //
27790    // So no caller has to know the spelling — every one of them already
27791    // reads the three numbers, and `IntervalKind::from_fields` names
27792    // what they mean.
27793    //
27794    // `inf` is NOT one of them, measured: `'inf'::interval` is *invalid
27795    // input syntax* on PostgreSQL 18.4 while `'inf'::float8` is
27796    // infinity. Interval takes the full word, in any case.
27797    {
27798        let word = s.trim();
27799        let word = word.strip_prefix('@').map_or(word, str::trim);
27800        let (neg, body) = match word.strip_prefix('-') {
27801            Some(rest) => (true, rest.trim_start()),
27802            None => (false, word.strip_prefix('+').map_or(word, str::trim_start)),
27803        };
27804        if body.eq_ignore_ascii_case("infinity") {
27805            return Some(if neg {
27806                (i32::MIN, i32::MIN, i64::MIN)
27807            } else {
27808                (i32::MAX, i32::MAX, i64::MAX)
27809            });
27810        }
27811    }
27812    // v7.39 (read01 timestamp.c) — PG's postgres_verbose forms: a leading
27813    // `@` is decorative; a trailing `ago` negates the whole interval.
27814    let mut trimmed = s.trim();
27815    trimmed = trimmed.strip_prefix('@').map_or(trimmed, str::trim);
27816    let mut negate = false;
27817    if let Some(rest) = trimmed
27818        .strip_suffix("ago")
27819        .filter(|r| r.ends_with(char::is_whitespace) || r.is_empty())
27820    {
27821        negate = true;
27822        trimmed = rest.trim();
27823    }
27824    let finish = |v: Option<(i32, i32, i64)>| -> Option<(i32, i32, i64)> {
27825        let (mo, d, us) = v?;
27826        if negate {
27827            Some((mo.checked_neg()?, d.checked_neg()?, us.checked_neg()?))
27828        } else {
27829            Some((mo, d, us))
27830        }
27831    };
27832    let s = trimmed;
27833    // ISO 8601 duration (`P1Y2M3DT4H`) and PG's year-month shorthand (`1-2`)
27834    // are single tokens, not the `<n> <unit>` pair form handled below.
27835    if let Some(rest) = trimmed.strip_prefix(['P', 'p']) {
27836        return finish(parse_iso8601_interval(rest));
27837    }
27838    if !trimmed.contains(char::is_whitespace) && trimmed.contains('-') {
27839        if let Some(iv) = parse_year_month_interval(trimmed) {
27840            return finish(Some(iv));
27841        }
27842    }
27843    // v7.39 (GUC knife 3, differential) — PG accepts a bare number as
27844    // SECONDS: `INTERVAL '0'` = 00:00:00, `INTERVAL '5'` = 00:00:05,
27845    // fractions kept to the microsecond (`'1.5'` = 00:00:01.5).
27846    if !trimmed.is_empty() && !trimmed.contains(char::is_whitespace) {
27847        if let Ok(n) = trimmed.parse::<i64>() {
27848            return finish(Some((0, 0, n.checked_mul(1_000_000)?)));
27849        }
27850        if let Ok(f) = trimmed.parse::<f64>() {
27851            if f.is_finite() {
27852                #[allow(clippy::cast_possible_truncation)]
27853                return finish(Some((0, 0, (f * 1_000_000.0) as i64)));
27854            }
27855        }
27856    }
27857    // v7.39 (round 243) — PG accepts the number and unit run together
27858    // (`'15h 2m 12s'`); split each token at the digit→letter boundary so
27859    // the `<n> <unit>` pair loop below sees them as two.
27860    let raw_parts: Vec<&str> = s.split_whitespace().collect();
27861    let mut parts: Vec<&str> = Vec::with_capacity(raw_parts.len());
27862    for p in raw_parts {
27863        let boundary = p
27864            .char_indices()
27865            .find(|(i, c)| {
27866                *i > 0
27867                    && c.is_ascii_alphabetic()
27868                    && p[..*i]
27869                        .chars()
27870                        .all(|d| d.is_ascii_digit() || matches!(d, '.' | '-' | '+'))
27871                    && p[..*i].chars().any(|d| d.is_ascii_digit())
27872            })
27873            .map(|(i, _)| i);
27874        match boundary {
27875            Some(i) => {
27876                parts.push(&p[..i]);
27877                parts.push(&p[i..]);
27878            }
27879            None => parts.push(p),
27880        }
27881    }
27882    // A bare clock-time token `HH:MM[:SS[.ffffff]]` carries the time-of-day
27883    // part (PG: `3 days 14:30:45`, or `14:30:45` alone). Extract it; whatever
27884    // remains is the `<n> <unit>` pair form handled below.
27885    let mut clock_us: i64 = 0;
27886    let mut had_clock = false;
27887    if let Some(pos) = parts.iter().position(|p| p.contains(':')) {
27888        clock_us = parse_interval_clock(parts[pos])?;
27889        parts.remove(pos);
27890        had_clock = true;
27891    }
27892    // v7.39 (read01 timestamp.c) — a lone bare number alongside a clock
27893    // time is DAYS (PG: '3 4:05:06' = 3 days 04:05:06).
27894    let mut lone_days: i32 = 0;
27895    if had_clock && parts.len() == 1 {
27896        if let Ok(n) = parts[0].parse::<i64>() {
27897            lone_days = i32::try_from(n).ok()?;
27898            parts.clear();
27899        }
27900    }
27901    if !parts.len().is_multiple_of(2) || (parts.is_empty() && !had_clock && lone_days == 0) {
27902        return None;
27903    }
27904    let mut months: i32 = 0;
27905    let mut days: i32 = lone_days;
27906    let mut micros: i64 = clock_us;
27907    let mut i = 0;
27908    while i < parts.len() {
27909        let unit_stripped = canonical_interval_unit(parts[i + 1])?;
27910        if let Ok(n) = parts[i].parse::<i64>() {
27911            match unit_stripped {
27912                "microsecond" => micros = micros.checked_add(n)?,
27913                "millisecond" => micros = micros.checked_add(n.checked_mul(1_000)?)?,
27914                "second" => micros = micros.checked_add(n.checked_mul(1_000_000)?)?,
27915                "minute" => micros = micros.checked_add(n.checked_mul(60_000_000)?)?,
27916                "hour" => micros = micros.checked_add(n.checked_mul(3_600_000_000)?)?,
27917                "day" => {
27918                    let n32 = i32::try_from(n).ok()?;
27919                    days = days.checked_add(n32)?;
27920                }
27921                "week" => {
27922                    let n32 = i32::try_from(n).ok()?;
27923                    days = days.checked_add(n32.checked_mul(7)?)?;
27924                }
27925                "month" => {
27926                    let n32 = i32::try_from(n).ok()?;
27927                    months = months.checked_add(n32)?;
27928                }
27929                "year" => {
27930                    let n32 = i32::try_from(n).ok()?;
27931                    months = months.checked_add(n32.checked_mul(12)?)?;
27932                }
27933                // v7.39 (read01 timestamp.c) — the larger calendar units.
27934                "decade" => {
27935                    let n32 = i32::try_from(n).ok()?;
27936                    months = months.checked_add(n32.checked_mul(120)?)?;
27937                }
27938                "century" => {
27939                    let n32 = i32::try_from(n).ok()?;
27940                    months = months.checked_add(n32.checked_mul(1200)?)?;
27941                }
27942                "millennium" => {
27943                    let n32 = i32::try_from(n).ok()?;
27944                    months = months.checked_add(n32.checked_mul(12000)?)?;
27945                }
27946                _ => return None,
27947            }
27948        } else if let Ok(f) = parts[i].parse::<f64>() {
27949            // Fractional units cascade down to the next-finer field the way
27950            // PG does: `1.5 days` -> `1 day 12:00:00`, `1.5 months` ->
27951            // `1 mon 15 days` (30-day month), `1.5 years` -> `1 year 6 mons`.
27952            // no_std: f64 has no trunc/fract/round methods, so do them with
27953            // casts (toward-zero) + explicit round-half-away-from-zero.
27954            #[allow(clippy::cast_possible_truncation)]
27955            fn round_i64(x: f64) -> i64 {
27956                if x >= 0.0 {
27957                    (x + 0.5) as i64
27958                } else {
27959                    (x - 0.5) as i64
27960                }
27961            }
27962            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27963            fn add_days_frac(days: &mut i32, micros: &mut i64, d: f64) -> Option<()> {
27964                const DAY_US: f64 = 86_400_000_000.0;
27965                let whole = d as i64; // truncates toward zero
27966                let frac = d - whole as f64;
27967                *days = days.checked_add(i32::try_from(whole).ok()?)?;
27968                *micros = micros.checked_add(round_i64(frac * DAY_US))?;
27969                Some(())
27970            }
27971            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
27972            match unit_stripped {
27973                "microsecond" => micros = micros.checked_add(round_i64(f))?,
27974                "millisecond" => micros = micros.checked_add(round_i64(f * 1_000.0))?,
27975                "second" => micros = micros.checked_add(round_i64(f * 1_000_000.0))?,
27976                "minute" => micros = micros.checked_add(round_i64(f * 60_000_000.0))?,
27977                "hour" => micros = micros.checked_add(round_i64(f * 3_600_000_000.0))?,
27978                "day" => add_days_frac(&mut days, &mut micros, f)?,
27979                "week" => add_days_frac(&mut days, &mut micros, f * 7.0)?,
27980                "month" => {
27981                    let whole = f as i64;
27982                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27983                    add_days_frac(&mut days, &mut micros, (f - whole as f64) * 30.0)?;
27984                }
27985                "year" => {
27986                    let m = f * 12.0;
27987                    let whole = m as i64;
27988                    months = months.checked_add(i32::try_from(whole).ok()?)?;
27989                    add_days_frac(&mut days, &mut micros, (m - whole as f64) * 30.0)?;
27990                }
27991                _ => return None,
27992            }
27993        } else {
27994            return None;
27995        }
27996        i += 2;
27997    }
27998    finish(Some((months, days, micros)))
27999}
28000
28001/// v7.37 — map a scalar type keyword to its [`CastTarget`] for the PG
28002/// `TYPE 'literal'` typed-literal syntax (`time '10:30'` == `'10:30'::time`).
28003/// `interval` is intentionally absent (handled by its own parser arm).
28004/// Returns `None` for names that aren't sensible as a bare typed literal, so
28005/// the caller falls back to treating the ident as a column reference.
28006fn typed_literal_cast_target(ident: &str) -> Option<CastTarget> {
28007    Some(match ident {
28008        "date" => CastTarget::Date,
28009        "timestamp" | "datetime" => CastTarget::Timestamp,
28010        "timestamptz" => CastTarget::Timestamptz,
28011        "bool" | "boolean" => CastTarget::Bool,
28012        "int" | "integer" | "int4" => CastTarget::Int,
28013        "bigint" | "int8" => CastTarget::BigInt,
28014        "float8" | "double precision" => CastTarget::Float,
28015        "uuid" => CastTarget::Uuid,
28016        "bytea" => CastTarget::Bytea,
28017        "json" => CastTarget::Json,
28018        "jsonb" => CastTarget::Jsonb,
28019        // Types without a dedicated CastTarget variant flow through the
28020        // generic Named path (engine resolves via column_type_to_data_type).
28021        "time" | "timetz" | "smallint" | "int2" | "numeric" | "decimal"
28022        | "real" | "float4" | "inet" | "cidr" | "macaddr" | "macaddr8"
28023        | "money" | "bit" | "varbit"
28024        // Geometric types accept the `TYPE 'literal'` prefix spelling too.
28025        | "point" | "line" | "lseg" | "box" | "path" | "polygon" | "circle"
28026        // Range / multirange types likewise.
28027        | "int4range" | "int8range" | "numrange" | "daterange" | "tsrange"
28028        | "tstzrange" | "int4multirange" | "int8multirange" | "nummultirange"
28029        | "datemultirange" | "tsmultirange" | "tstzmultirange"
28030        // v7.39 (read01 round 18) — oid / name / jsonpath literal prefixes.
28031        | "oid" | "name" | "jsonpath" | "pg_lsn" | "varchar" | "text" | "xid" | "xid8" => {
28032            CastTarget::Named(alloc::string::String::from(ident))
28033        }
28034        _ => return None,
28035    })
28036}
28037
28038/// v7.12.4 — map a bare type-name identifier (the form that
28039/// appears in a function arg list or RETURNS clause) to a
28040/// [`ColumnTypeName`]. Returns `None` for unknown / extension
28041/// types so the caller can preserve them as
28042/// [`FunctionArgType::Raw`] / [`FunctionReturn::Other`].
28043///
28044/// Subset of the full column-type grammar — we deliberately
28045/// don't parse parameterised forms (`VARCHAR(n)`, `NUMERIC(p,s)`)
28046/// here because function-arg types in v7.12.4 are mostly the
28047/// bare form (`text`, `int`, `bytea`, …).
28048/// v7.39 (round 315, V19) — does this whole phrase name a type, rather
28049/// than being `name TYPE`?
28050///
28051/// The multi-word spellings SQL allows for a bare argument type, each
28052/// verified accepted by PG 18.4 as `CREATE FUNCTION f(<phrase>)`.
28053///
28054/// NOTE this list also exists in `spg-storage`, which computes the
28055/// signature key from the rendered argument text and has to reach the
28056/// same verdict. The two crates are siblings — neither depends on the
28057/// other — and each already carries its own table of type spellings
28058/// (`map_type_ident_to_column_type_name` here, `normalize_type_name`
28059/// there), so this follows the structure rather than inventing new
28060/// duplication. Recorded as V49.
28061pub fn is_multiword_type_phrase(phrase: &str) -> bool {
28062    let t = phrase.trim().to_ascii_lowercase();
28063    let base = t.split_once('(').map_or(t.as_str(), |(h, _)| h).trim();
28064    matches!(
28065        base,
28066        "double precision"
28067            | "character varying"
28068            | "bit varying"
28069            | "timestamp with time zone"
28070            | "timestamp without time zone"
28071            | "time with time zone"
28072            | "time without time zone"
28073            | "national character"
28074            | "national character varying"
28075    )
28076}
28077
28078fn map_type_ident_to_column_type_name(ident: &str) -> Option<ColumnTypeName> {
28079    Some(match ident.to_ascii_lowercase().as_str() {
28080        "smallint" | "tinyint" => ColumnTypeName::SmallInt,
28081        "int" | "integer" | "mediumint" => ColumnTypeName::Int,
28082        "bigint" => ColumnTypeName::BigInt,
28083        "float" | "double" => ColumnTypeName::Float,
28084        // v7.39 (round 269) — real is 32-bit.
28085        "real" | "float4" => ColumnTypeName::Real,
28086        "text" => ColumnTypeName::Text,
28087        "bool" | "boolean" => ColumnTypeName::Bool,
28088        "date" => ColumnTypeName::Date,
28089        "timestamp" | "datetime" => ColumnTypeName::Timestamp,
28090        "timestamptz" => ColumnTypeName::Timestamptz,
28091        "json" => ColumnTypeName::Json,
28092        "jsonb" => ColumnTypeName::Jsonb,
28093        "bytea" | "bytes" => ColumnTypeName::Bytes,
28094        "tsvector" => ColumnTypeName::TsVector,
28095        "tsquery" => ColumnTypeName::TsQuery,
28096        "uuid" => ColumnTypeName::Uuid,
28097        "interval" => ColumnTypeName::Interval,
28098        "time" => ColumnTypeName::Time,
28099        "year" => ColumnTypeName::Year,
28100        "timetz" => ColumnTypeName::TimeTz,
28101        "money" => ColumnTypeName::Money,
28102        _ => return None,
28103    })
28104}
28105
28106/// v7.12.4 — parse a PL/pgSQL function body (the bytes between
28107/// `$$ ... $$`). Returns the parsed `BEGIN ... END;` block.
28108///
28109/// v7.12.4 grammar (strict subset — IF / LOOP / DECLARE / RAISE
28110/// / embedded SQL land in v7.12.5+):
28111///
28112/// ```text
28113///   body          := [ws] block [ws]
28114///   block         := BEGIN stmt ( ; stmt )* [ ; ] END [ ; ]
28115///   stmt          := assign | return
28116///   assign        := assign_target := expr
28117///   assign_target := ( NEW | OLD ) . ident | ident
28118///   return        := RETURN ( NEW | OLD | NULL | expr )
28119/// ```
28120///
28121/// `expr` is parsed by recursing into the regular `Parser` — so a
28122/// PL/pgSQL `NEW.search_vector := to_tsvector('english',
28123/// NEW.subject || ' ' || NEW.sender)` body shape works without
28124/// the body parser knowing what `to_tsvector` is.
28125///
28126/// Errors here cause the caller to fall back to
28127/// `FunctionBody::Raw` — keeping the CREATE FUNCTION DDL itself
28128/// successful, but the executor will refuse to invoke the
28129/// function with an "unparseable body" error.
28130/// v7.12.4 — public alias for [`parse_plpgsql_body`] re-exported
28131/// from the crate root as `spg_sql::parse_function_body`.
28132pub fn parse_function_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28133    parse_plpgsql_body(body)
28134}
28135
28136fn parse_plpgsql_body(body: &str) -> Result<PlPgSqlBlock, ParseError> {
28137    // Use the regular lexer on the body text. The trailing
28138    // `END;` may or may not have a semicolon; the lexer treats
28139    // both forms identically.
28140    let tokens = lexer::tokenize(body).map_err(|e| ParseError {
28141        message: alloc::format!("plpgsql body lex error: {e}"),
28142        token_pos: 0,
28143    })?;
28144    let mut parser = Parser::new(tokens);
28145    parser.parse_plpgsql_block()
28146}
28147
28148/// v7.39 (GUC) — the textual body of a SET value, for list joining.
28149fn set_value_text(v: &crate::ast::SetValue) -> alloc::string::String {
28150    match v {
28151        crate::ast::SetValue::String(s)
28152        | crate::ast::SetValue::Ident(s)
28153        | crate::ast::SetValue::Number(s) => s.clone(),
28154        crate::ast::SetValue::Default => "DEFAULT".into(),
28155        crate::ast::SetValue::Null => "NULL".into(),
28156    }
28157}
28158
28159/// v7.39 (round 145, parse_cte.c / parse_agg.c) — true when an expression
28160/// contains an aggregate call at ITS OWN query level (recursion stops at
28161/// sublink boundaries — a sublink's aggregates belong to the sublink).
28162/// Backs the "aggregate functions are not allowed in a recursive query's
28163/// recursive term" well-formedness check.
28164/// v7.40.0 — is this the name of an aggregate? The same list
28165/// `expr_has_toplevel_aggregate` walks, exposed so the grouping-set
28166/// rewrite can leave an aggregate's ARGUMENT alone.
28167pub(crate) fn is_aggregate_function_name(name: &str) -> bool {
28168    AGG_NAMES.iter().any(|a| name.eq_ignore_ascii_case(a))
28169}
28170
28171const AGG_NAMES: &[&str] = &[
28172    "count",
28173    "sum",
28174    "min",
28175    "max",
28176    "avg",
28177    "string_agg",
28178    "array_agg",
28179    "bool_and",
28180    "bool_or",
28181    "every",
28182    "any_value",
28183    "json_agg",
28184    "jsonb_agg",
28185    "json_object_agg",
28186    "jsonb_object_agg",
28187    "bit_and",
28188    "bit_or",
28189    "bit_xor",
28190    "var_pop",
28191    "var_samp",
28192    "variance",
28193    "std",
28194    "stddev",
28195    "stddev_pop",
28196    "stddev_samp",
28197    "range_agg",
28198    "range_intersect_agg",
28199    "percentile_cont",
28200    "percentile_disc",
28201    "mode",
28202    "corr",
28203    "covar_pop",
28204    "covar_samp",
28205];
28206
28207fn expr_has_toplevel_aggregate(e: &Expr) -> bool {
28208    match e {
28209        Expr::AggregateOrdered { .. } => true,
28210        Expr::FunctionCall { name, args } => {
28211            AGG_NAMES.contains(&name.to_ascii_lowercase().as_str())
28212                || args.iter().any(expr_has_toplevel_aggregate)
28213        }
28214        Expr::NamedArg { expr, .. }
28215        | Expr::Variadic(expr)
28216        | Expr::Unary { expr, .. }
28217        | Expr::Cast { expr, .. }
28218        | Expr::IsNull { expr, .. }
28219        | Expr::FieldAccess { base: expr, .. }
28220        | Expr::Extract { source: expr, .. } => expr_has_toplevel_aggregate(expr),
28221        Expr::Binary { lhs, rhs, .. } => {
28222            expr_has_toplevel_aggregate(lhs) || expr_has_toplevel_aggregate(rhs)
28223        }
28224        Expr::Like { expr, pattern, .. } => {
28225            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(pattern)
28226        }
28227        Expr::Array(items) => items.iter().any(expr_has_toplevel_aggregate),
28228        Expr::InList { expr, list, .. } => {
28229            expr_has_toplevel_aggregate(expr) || list.iter().any(expr_has_toplevel_aggregate)
28230        }
28231        Expr::ArraySubscript { target, index } => {
28232            expr_has_toplevel_aggregate(target) || expr_has_toplevel_aggregate(index)
28233        }
28234        Expr::ArraySlice { target, lo, hi } => {
28235            expr_has_toplevel_aggregate(target)
28236                || lo.as_deref().is_some_and(expr_has_toplevel_aggregate)
28237                || hi.as_deref().is_some_and(expr_has_toplevel_aggregate)
28238        }
28239        Expr::AnyAll { expr, array, .. } => {
28240            expr_has_toplevel_aggregate(expr) || expr_has_toplevel_aggregate(array)
28241        }
28242        Expr::Case {
28243            operand,
28244            branches,
28245            else_branch,
28246        } => {
28247            operand.as_deref().is_some_and(expr_has_toplevel_aggregate)
28248                || branches
28249                    .iter()
28250                    .any(|(w, t)| expr_has_toplevel_aggregate(w) || expr_has_toplevel_aggregate(t))
28251                || else_branch
28252                    .as_deref()
28253                    .is_some_and(expr_has_toplevel_aggregate)
28254        }
28255        // The outer-level operands of a sublink can aggregate; the sublink's
28256        // own body cannot leak its aggregates up here.
28257        Expr::InSubquery { expr, .. } => expr_has_toplevel_aggregate(expr),
28258        Expr::RowInSubquery { row, .. } | Expr::RowCmpSubquery { row, .. } => {
28259            row.iter().any(expr_has_toplevel_aggregate)
28260        }
28261        _ => false,
28262    }
28263}
28264
28265/// v7.39 (round 145, parse_cte.c) — true when any sublink expression
28266/// (EXISTS / IN / scalar subquery) inside this SELECT term references the
28267/// named table anywhere in its subtree. A plain FROM derived table is NOT a
28268/// sublink and is legal in a recursive term, so it is not walked here.
28269fn select_has_self_ref_in_sublink(s: &crate::ast::SelectStatement, name: &str) -> bool {
28270    let mut exprs: Vec<&Expr> = Vec::new();
28271    for it in &s.items {
28272        if let crate::ast::SelectItem::Expr { expr, .. } = it {
28273            exprs.push(expr);
28274        }
28275    }
28276    if let Some(w) = &s.where_ {
28277        exprs.push(w);
28278    }
28279    if let Some(h) = &s.having {
28280        exprs.push(h);
28281    }
28282    if let Some(g) = &s.group_by {
28283        exprs.extend(g.iter());
28284    }
28285    if let Some(from) = &s.from {
28286        for j in &from.joins {
28287            if let Some(on) = &j.on {
28288                exprs.push(on);
28289            }
28290        }
28291    }
28292    exprs.into_iter().any(|e| expr_sublink_mentions(e, name))
28293}
28294
28295/// Does this expression contain a sublink whose subquery mentions `name`?
28296fn expr_sublink_mentions(e: &Expr, name: &str) -> bool {
28297    match e {
28298        Expr::ScalarSubquery(sub) => select_mentions_table(sub, name),
28299        Expr::Exists { subquery, .. } => select_mentions_table(subquery, name),
28300        Expr::InSubquery { expr, subquery, .. } => {
28301            expr_sublink_mentions(expr, name) || select_mentions_table(subquery, name)
28302        }
28303        Expr::RowInSubquery { row, subquery, .. } => {
28304            row.iter().any(|x| expr_sublink_mentions(x, name))
28305                || select_mentions_table(subquery, name)
28306        }
28307        Expr::RowCmpSubquery { row, subquery, .. } => {
28308            row.iter().any(|x| expr_sublink_mentions(x, name))
28309                || select_mentions_table(subquery, name)
28310        }
28311        Expr::NamedArg { expr, .. }
28312        | Expr::Variadic(expr)
28313        | Expr::Unary { expr, .. }
28314        | Expr::Cast { expr, .. }
28315        | Expr::IsNull { expr, .. }
28316        | Expr::FieldAccess { base: expr, .. }
28317        | Expr::Extract { source: expr, .. } => expr_sublink_mentions(expr, name),
28318        Expr::Binary { lhs, rhs, .. } => {
28319            expr_sublink_mentions(lhs, name) || expr_sublink_mentions(rhs, name)
28320        }
28321        Expr::Like { expr, pattern, .. } => {
28322            expr_sublink_mentions(expr, name) || expr_sublink_mentions(pattern, name)
28323        }
28324        Expr::FunctionCall { args, .. } | Expr::Array(args) => {
28325            args.iter().any(|x| expr_sublink_mentions(x, name))
28326        }
28327        Expr::InList { expr, list, .. } => {
28328            expr_sublink_mentions(expr, name) || list.iter().any(|x| expr_sublink_mentions(x, name))
28329        }
28330        Expr::ArraySubscript { target, index } => {
28331            expr_sublink_mentions(target, name) || expr_sublink_mentions(index, name)
28332        }
28333        Expr::ArraySlice { target, lo, hi } => {
28334            expr_sublink_mentions(target, name)
28335                || lo
28336                    .as_deref()
28337                    .is_some_and(|x| expr_sublink_mentions(x, name))
28338                || hi
28339                    .as_deref()
28340                    .is_some_and(|x| expr_sublink_mentions(x, name))
28341        }
28342        Expr::AnyAll { expr, array, .. } => {
28343            expr_sublink_mentions(expr, name) || expr_sublink_mentions(array, name)
28344        }
28345        Expr::Case {
28346            operand,
28347            branches,
28348            else_branch,
28349        } => {
28350            operand
28351                .as_deref()
28352                .is_some_and(|x| expr_sublink_mentions(x, name))
28353                || branches
28354                    .iter()
28355                    .any(|(w, t)| expr_sublink_mentions(w, name) || expr_sublink_mentions(t, name))
28356                || else_branch
28357                    .as_deref()
28358                    .is_some_and(|x| expr_sublink_mentions(x, name))
28359        }
28360        _ => false,
28361    }
28362}
28363
28364/// Does this SELECT (in full — FROM tables, derived tables, its own
28365/// sublinks, and union arms) mention the named table?
28366fn select_mentions_table(s: &crate::ast::SelectStatement, name: &str) -> bool {
28367    if let Some(from) = &s.from {
28368        if from.primary.name.eq_ignore_ascii_case(name) {
28369            return true;
28370        }
28371        if let Some(sub) = &from.primary.lateral_subquery
28372            && select_mentions_table(sub, name)
28373        {
28374            return true;
28375        }
28376        for j in &from.joins {
28377            if j.table.name.eq_ignore_ascii_case(name) {
28378                return true;
28379            }
28380            if let Some(sub) = &j.table.lateral_subquery
28381                && select_mentions_table(sub, name)
28382            {
28383                return true;
28384            }
28385        }
28386    }
28387    if select_has_self_ref_in_sublink(s, name) {
28388        return true;
28389    }
28390    s.unions.iter().any(|(_, u)| select_mentions_table(u, name))
28391}
28392
28393/// v7.39 (round 284) — fold a constant `LIMIT` / `OFFSET` expression to a
28394/// row count, the way PG evaluates one before applying it.
28395///
28396/// `None` = not a constant (a column, a subquery, a function call).
28397/// `Some(Err(msg))` = PG rejects it, and the message is PG's; `{L}` in the
28398/// message stands in for LIMIT / OFFSET, which the caller substitutes.
28399/// All wordings were read off live PG 18.4.
28400fn fold_limit_constant(e: &crate::ast::Expr) -> Option<Result<i128, alloc::string::String>> {
28401    use crate::ast::{BinOp, Expr, Literal, UnOp};
28402    match e {
28403        Expr::Literal(Literal::Integer(n)) => Some(Ok(i128::from(*n))),
28404        Expr::Literal(Literal::Numeric { unscaled, scale }) => {
28405            Some(Ok(round_scaled_half_away(*unscaled, *scale)))
28406        }
28407        // PG coerces a string by its CONTENT, and fails on the value.
28408        Expr::Literal(Literal::String(t)) => Some(t.trim().parse::<i64>().map_or_else(
28409            |_| {
28410                Err(alloc::format!(
28411                    "invalid input syntax for type bigint: \"{t}\""
28412                ))
28413            },
28414            |n| Ok(i128::from(n)),
28415        )),
28416        Expr::Literal(Literal::Bool(_)) => Some(Err(
28417            "argument of {L} must be type bigint, not type boolean".into(),
28418        )),
28419        Expr::Unary {
28420            op: UnOp::Neg,
28421            expr,
28422        } => match fold_limit_constant(expr)? {
28423            Ok(v) => Some(Ok(-v)),
28424            e @ Err(_) => Some(e),
28425        },
28426        Expr::Binary { lhs, op, rhs } => {
28427            let a = match fold_limit_constant(lhs)? {
28428                Ok(v) => v,
28429                e @ Err(_) => return Some(e),
28430            };
28431            let b = match fold_limit_constant(rhs)? {
28432                Ok(v) => v,
28433                e @ Err(_) => return Some(e),
28434            };
28435            let out = match op {
28436                BinOp::Add => a.checked_add(b),
28437                BinOp::Sub => a.checked_sub(b),
28438                BinOp::Mul => a.checked_mul(b),
28439                BinOp::Div if b != 0 => a.checked_div(b),
28440                BinOp::Div => return Some(Err("division by zero".into())),
28441                BinOp::Mod if b != 0 => a.checked_rem(b),
28442                BinOp::Mod => return Some(Err("division by zero".into())),
28443                _ => return None,
28444            };
28445            // PG evaluates the arithmetic in the operand's own type, so an
28446            // int-by-int product that leaves int range fails there — before
28447            // the row count is ever looked at.
28448            match out {
28449                Some(v) if v > i128::from(i32::MAX) || v < i128::from(i32::MIN) => {
28450                    Some(Err("integer out of range".into()))
28451                }
28452                Some(v) => Some(Ok(v)),
28453                None => Some(Err("integer out of range".into())),
28454            }
28455        }
28456        _ => None,
28457    }
28458}
28459
28460/// Round `unscaled / 10^scale` half away from zero — PG's numeric→bigint
28461/// cast, which is what makes `LIMIT 2.5` keep three rows.
28462fn round_scaled_half_away(unscaled: i128, scale: u16) -> i128 {
28463    if scale == 0 {
28464        return unscaled;
28465    }
28466    let Some(div) = 10i128.checked_pow(u32::from(scale)) else {
28467        return 0;
28468    };
28469    let neg = unscaled < 0;
28470    let mag = unscaled.unsigned_abs() as i128;
28471    let rounded = (mag + div / 2) / div;
28472    if neg { -rounded } else { rounded }
28473}
28474
28475#[cfg(test)]
28476mod tests {
28477    use super::*;
28478    use alloc::string::ToString;
28479
28480    fn parse(s: &str) -> Statement {
28481        parse_statement(s).expect("parse ok")
28482    }
28483
28484    // v7.37.43-T4 sentori cutover acceptance — `release`, `index`,
28485    // `tables`, `partition`, etc. are unreserved keywords per PG's
28486    // `pg_get_keywords()` and MUST be usable as column / table /
28487    // alias names. Pre-T4 every drop-in user whose schema had one
28488    // of these as a column name (sentori events.release, mailrs
28489    // messages.index in some forks) blew the parser up at CREATE
28490    // TABLE time with "expected identifier, got Release". The
28491    // generalisation lives in `unreserved_keyword_text` + the
28492    // `expect_ident_like` and `parse_atom` arms that consult it.
28493    #[test]
28494    fn release_usable_as_column_name_in_create_table() {
28495        let stmt =
28496            parse("CREATE TABLE events (id INT PRIMARY KEY, release TEXT NOT NULL, payload TEXT)");
28497        if let Statement::CreateTable(t) = stmt {
28498            let names: alloc::vec::Vec<&str> = t.columns.iter().map(|c| c.name.as_str()).collect();
28499            assert_eq!(names, alloc::vec!["id", "release", "payload"]);
28500        } else {
28501            panic!("expected CreateTable");
28502        }
28503    }
28504
28505    #[test]
28506    fn release_usable_as_column_ref_in_select_projection() {
28507        // The sentori `0003_partition_events.sql` INSERT-SELECT
28508        // walk references `release` in both column lists; the
28509        // projection-side use exercises `parse_atom`'s relaxed
28510        // identifier set.
28511        parse("SELECT id, release, payload FROM events WHERE id = 1");
28512    }
28513
28514    #[test]
28515    fn release_usable_as_column_ref_in_insert_column_list() {
28516        // INSERT INTO t (id, release, payload) VALUES (…)
28517        parse("INSERT INTO events (id, release, payload) VALUES (1, '1.0.0', 'data')");
28518    }
28519
28520    #[test]
28521    fn alter_column_drop_not_null_uses_keyword_drop_token() {
28522        // Sentori `0013_audit_tombstone.sql` issues
28523        // `ALTER TABLE … ALTER COLUMN x DROP NOT NULL`. The lexer
28524        // emits Token::Drop (not Ident("drop")); the parser must
28525        // accept both in the ALTER COLUMN sub-dispatch.
28526        parse("ALTER TABLE audit_logs ALTER COLUMN org_id DROP NOT NULL");
28527    }
28528
28529    #[test]
28530    fn create_index_accepts_parenthesised_expression_key() {
28531        // sentori `0040_events_bundle_idx.sql` shape — JSONB
28532        // expression index. Pre-T4 the parser bailed at the
28533        // inner `(` with "expected column ident or expression,
28534        // got LParen". The Token::LParen arm in CREATE INDEX
28535        // routes through the expression parser instead.
28536        parse(
28537            "CREATE INDEX IF NOT EXISTS events_bundle_id_idx \
28538             ON events ((payload->'bundle'->>'id'))",
28539        );
28540    }
28541
28542    // v7.30.2 (mailrs round-25 ask 2) — nesting / chain budgets must
28543    // surface as parse errors, never stack overflows (embed hosts
28544    // abort on overflow).
28545    /// The nesting budget is a COUNT; what it has to fit inside is a
28546    /// number of BYTES, and only one of those two is stable across
28547    /// compiler versions. Round 847 measured 30,336 bytes per level
28548    /// after a toolchain move, which puts 64 levels at 1.94 MB and
28549    /// overflows a 2 MiB thread — `nesting_budget_errors_cleanly`
28550    /// aborted instead of erroring, which is precisely the outcome it
28551    /// exists to rule out.
28552    ///
28553    /// So the budget is metered rather than assumed. The ceiling leaves
28554    /// the depth SPG advertises fitting in a default 2 MiB thread with
28555    /// room to spare, in the debug build, where frames are widest.
28556    #[test]
28557    fn nesting_frame_cost_stays_under_ceiling() {
28558        // Room for MAX_NEST_DEPTH levels inside 1.2 MB, so a 2 MiB
28559        // thread keeps a margin for whatever called the parser.
28560        const CEILING: usize = 1_200_000 / MAX_NEST_DEPTH;
28561
28562        frame_meter::reset();
28563        let depth = frame_meter::SAMPLE_HI + 8;
28564        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28565        parse(&sql);
28566
28567        let per_level = frame_meter::bytes_per_level();
28568        {
28569            extern crate std;
28570            std::eprintln!("nesting frame: {per_level} bytes/level, ceiling {CEILING}");
28571        }
28572        assert!(
28573            per_level <= CEILING,
28574            "{per_level} bytes per nesting level exceeds {CEILING}; \
28575             {MAX_NEST_DEPTH} levels would want {} bytes. Out-line arms \
28576             in parse_expr_inner / parse_unary rather than lowering the \
28577             depth or widening the stack.",
28578            per_level * MAX_NEST_DEPTH
28579        );
28580    }
28581
28582    #[test]
28583    fn nesting_budget_errors_cleanly() {
28584        let depth = MAX_NEST_DEPTH + 50;
28585        let sql = format!("SELECT {}1{}", "(".repeat(depth), ")".repeat(depth));
28586        let err = parse_statement(&sql).expect_err("must reject");
28587        assert!(err.message.contains("nests deeper"), "{err:?}");
28588        // Within budget still parses.
28589        let sql = format!("SELECT {}1{}", "(".repeat(48), ")".repeat(48));
28590        parse(&sql);
28591    }
28592
28593    #[test]
28594    fn binary_chain_budget_errors_cleanly() {
28595        let sql = format!("SELECT 1{}", " + 1".repeat(MAX_BINARY_CHAIN + 50));
28596        let err = parse_statement(&sql).expect_err("must reject");
28597        assert!(err.message.contains("chained binary"), "{err:?}");
28598        // Within budget still parses (chain depth ≤ budget is safe
28599        // for recursive eval/drop on 2 MiB stacks).
28600        let sql = format!("SELECT 1{}", " + 1".repeat(200));
28601        parse(&sql);
28602    }
28603
28604    #[test]
28605    fn in_list_unaffected_by_chain_budget() {
28606        // Flat InList: 20k elements parse fine and stay flat.
28607        let items: alloc::vec::Vec<String> = (0..20_000).map(|k| k.to_string()).collect();
28608        let sql = format!("SELECT 1 WHERE 5 IN ({})", items.join(","));
28609        let Statement::Select(s) = parse(&sql) else {
28610            panic!("expected select")
28611        };
28612        let Some(Expr::InList { list, negated, .. }) = s.where_ else {
28613            panic!("expected flat InList, got {:?}", s.where_)
28614        };
28615        assert_eq!(list.len(), 20_000);
28616        assert!(!negated);
28617    }
28618
28619    fn lit_int(n: i64) -> Expr {
28620        Expr::Literal(Literal::Integer(n))
28621    }
28622
28623    fn col(name: &str) -> Expr {
28624        Expr::Column(ColumnName {
28625            qualifier: None,
28626            name: name.into(),
28627        })
28628    }
28629
28630    #[test]
28631    fn select_single_integer() {
28632        let s = parse("SELECT 1");
28633        let Statement::Select(s) = s else {
28634            panic!("expected SELECT")
28635        };
28636        assert_eq!(s.items.len(), 1);
28637        assert!(s.from.is_none());
28638        assert!(s.where_.is_none());
28639    }
28640
28641    #[test]
28642    fn select_multiple_literal_kinds() {
28643        let s = parse("SELECT 1, 'hi', NULL, TRUE, 1.5");
28644        let Statement::Select(s) = s else {
28645            panic!("expected SELECT")
28646        };
28647        assert_eq!(s.items.len(), 5);
28648    }
28649
28650    #[test]
28651    fn select_wildcard_from_table() {
28652        let s = parse("SELECT * FROM users");
28653        let Statement::Select(s) = s else {
28654            panic!("expected SELECT")
28655        };
28656        assert!(matches!(s.items[..], [SelectItem::Wildcard]));
28657        assert_eq!(s.from.as_ref().unwrap().primary.name, "users");
28658    }
28659
28660    #[test]
28661    fn select_with_table_alias() {
28662        let s = parse("SELECT * FROM users AS u");
28663        let Statement::Select(s) = s else {
28664            panic!("expected SELECT")
28665        };
28666        let t = &s.from.as_ref().unwrap().primary;
28667        assert_eq!(t.name, "users");
28668        assert_eq!(t.alias.as_deref(), Some("u"));
28669    }
28670
28671    #[test]
28672    fn select_with_where_eq() {
28673        let s = parse("SELECT a FROM t WHERE a = 1");
28674        let Statement::Select(s) = s else {
28675            panic!("expected SELECT")
28676        };
28677        let w = s.where_.unwrap();
28678        assert_eq!(
28679            w,
28680            Expr::Binary {
28681                lhs: Box::new(col("a")),
28682                op: BinOp::Eq,
28683                rhs: Box::new(lit_int(1)),
28684            }
28685        );
28686    }
28687
28688    #[test]
28689    fn arithmetic_precedence() {
28690        let s = parse("SELECT 1 + 2 * 3");
28691        let Statement::Select(s) = s else {
28692            panic!("expected SELECT")
28693        };
28694        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28695            panic!("wildcard?")
28696        };
28697        assert_eq!(
28698            expr,
28699            &Expr::Binary {
28700                lhs: Box::new(lit_int(1)),
28701                op: BinOp::Add,
28702                rhs: Box::new(Expr::Binary {
28703                    lhs: Box::new(lit_int(2)),
28704                    op: BinOp::Mul,
28705                    rhs: Box::new(lit_int(3)),
28706                }),
28707            }
28708        );
28709    }
28710
28711    #[test]
28712    fn parentheses_override_precedence() {
28713        let s = parse("SELECT (1 + 2) * 3");
28714        let Statement::Select(s) = s else {
28715            panic!("expected SELECT")
28716        };
28717        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28718            panic!()
28719        };
28720        assert_eq!(
28721            expr,
28722            &Expr::Binary {
28723                lhs: Box::new(Expr::Binary {
28724                    lhs: Box::new(lit_int(1)),
28725                    op: BinOp::Add,
28726                    rhs: Box::new(lit_int(2)),
28727                }),
28728                op: BinOp::Mul,
28729                rhs: Box::new(lit_int(3)),
28730            }
28731        );
28732    }
28733
28734    #[test]
28735    fn not_binds_below_comparison() {
28736        // `NOT a = 1` should parse as `NOT (a = 1)`.
28737        let s = parse("SELECT NOT a = 1 FROM t");
28738        let Statement::Select(s) = s else {
28739            panic!("expected SELECT")
28740        };
28741        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28742            panic!()
28743        };
28744        assert_eq!(
28745            expr,
28746            &Expr::Unary {
28747                op: UnOp::Not,
28748                expr: Box::new(Expr::Binary {
28749                    lhs: Box::new(col("a")),
28750                    op: BinOp::Eq,
28751                    rhs: Box::new(lit_int(1)),
28752                }),
28753            }
28754        );
28755    }
28756
28757    #[test]
28758    fn unary_minus_binds_above_multiplication() {
28759        // `-a * 2` should be `(-a) * 2`.
28760        let s = parse("SELECT -a * 2 FROM t");
28761        let Statement::Select(s) = s else {
28762            panic!("expected SELECT")
28763        };
28764        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28765            panic!()
28766        };
28767        assert_eq!(
28768            expr,
28769            &Expr::Binary {
28770                lhs: Box::new(Expr::Unary {
28771                    op: UnOp::Neg,
28772                    expr: Box::new(col("a")),
28773                }),
28774                op: BinOp::Mul,
28775                rhs: Box::new(lit_int(2)),
28776            }
28777        );
28778    }
28779
28780    #[test]
28781    fn qualified_column() {
28782        let s = parse("SELECT t.col FROM t");
28783        let Statement::Select(s) = s else {
28784            panic!("expected SELECT")
28785        };
28786        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28787            panic!()
28788        };
28789        assert_eq!(
28790            expr,
28791            &Expr::Column(ColumnName {
28792                qualifier: Some("t".into()),
28793                name: "col".into()
28794            })
28795        );
28796    }
28797
28798    #[test]
28799    fn select_item_alias_with_as() {
28800        let s = parse("SELECT a AS y FROM t");
28801        let Statement::Select(s) = s else {
28802            panic!("expected SELECT")
28803        };
28804        let SelectItem::Expr { alias, .. } = &s.items[0] else {
28805            panic!()
28806        };
28807        assert_eq!(alias.as_deref(), Some("y"));
28808    }
28809
28810    #[test]
28811    fn trailing_semicolon_accepted() {
28812        let s = parse("SELECT 1;");
28813        let Statement::Select(s) = s else {
28814            panic!("expected SELECT")
28815        };
28816        assert_eq!(s.items.len(), 1);
28817    }
28818
28819    #[test]
28820    fn boolean_chain_with_and_or_not() {
28821        // (NOT a) OR (b AND (NOT c))
28822        let s = parse("SELECT NOT a OR b AND NOT c FROM t");
28823        let Statement::Select(s) = s else {
28824            panic!("expected SELECT")
28825        };
28826        let SelectItem::Expr { expr, .. } = &s.items[0] else {
28827            panic!()
28828        };
28829        let expected = Expr::Binary {
28830            lhs: Box::new(Expr::Unary {
28831                op: UnOp::Not,
28832                expr: Box::new(col("a")),
28833            }),
28834            op: BinOp::Or,
28835            rhs: Box::new(Expr::Binary {
28836                lhs: Box::new(col("b")),
28837                op: BinOp::And,
28838                rhs: Box::new(Expr::Unary {
28839                    op: UnOp::Not,
28840                    expr: Box::new(col("c")),
28841                }),
28842            }),
28843        };
28844        assert_eq!(expr, &expected);
28845    }
28846
28847    #[test]
28848    fn empty_input_errors() {
28849        // v7.14.0 — pg_dump preambles emit several comment-only
28850        // / blank-line statements that collapse to Statement::
28851        // Empty rather than a parse error. The old "SELECT in
28852        // message" assertion is stale; verify the new contract:
28853        // empty / whitespace / comment-only input parses to
28854        // Statement::Empty.
28855        assert!(matches!(parse_statement("").unwrap(), Statement::Empty));
28856        assert!(matches!(
28857            parse_statement("  \n\t ").unwrap(),
28858            Statement::Empty
28859        ));
28860        // Sanity: malformed-but-non-empty still errors.
28861        assert!(parse_statement("SELECT FROM WHERE").is_err());
28862    }
28863
28864    #[test]
28865    fn unmatched_paren_errors() {
28866        assert!(parse_statement("SELECT (1 + 2").is_err());
28867    }
28868
28869    #[test]
28870    fn display_round_trip_simple_select() {
28871        let original = parse("SELECT a + 1 FROM t WHERE a > 0");
28872        let text = original.to_string();
28873        let again = parse_statement(&text).expect("re-parse");
28874        assert_eq!(original, again);
28875    }
28876
28877    // --- CREATE TABLE & INSERT (v0.3) ---------------------------------------
28878
28879    #[test]
28880    fn create_table_single_column() {
28881        let s = parse("CREATE TABLE foo (a INT)");
28882        let Statement::CreateTable(c) = s else {
28883            panic!("expected CreateTable")
28884        };
28885        assert_eq!(c.name, "foo");
28886        assert_eq!(c.columns.len(), 1);
28887        assert_eq!(c.columns[0].name, "a");
28888        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28889        assert!(c.columns[0].nullable);
28890    }
28891
28892    #[test]
28893    fn create_table_multi_column_with_not_null_mix() {
28894        let s = parse("CREATE TABLE u (id INT NOT NULL, name TEXT, score FLOAT NOT NULL, ok BOOL)");
28895        let Statement::CreateTable(c) = s else {
28896            panic!()
28897        };
28898        assert_eq!(c.columns.len(), 4);
28899        assert_eq!(c.columns[0].ty, ColumnTypeName::Int);
28900        assert!(!c.columns[0].nullable);
28901        assert_eq!(c.columns[1].ty, ColumnTypeName::Text);
28902        assert!(c.columns[1].nullable);
28903        assert_eq!(c.columns[2].ty, ColumnTypeName::Float);
28904        assert!(!c.columns[2].nullable);
28905        assert_eq!(c.columns[3].ty, ColumnTypeName::Bool);
28906    }
28907
28908    #[test]
28909    fn create_table_bigint_supported() {
28910        let s = parse("CREATE TABLE accounts (id BIGINT NOT NULL)");
28911        let Statement::CreateTable(c) = s else {
28912            panic!()
28913        };
28914        assert_eq!(c.columns[0].ty, ColumnTypeName::BigInt);
28915    }
28916
28917    #[test]
28918    fn create_table_vector_default_is_f32() {
28919        let s = parse("CREATE TABLE t (v VECTOR(128))");
28920        let Statement::CreateTable(c) = s else {
28921            panic!()
28922        };
28923        assert_eq!(
28924            c.columns[0].ty,
28925            ColumnTypeName::Vector {
28926                dim: 128,
28927                encoding: VecEncoding::F32,
28928            },
28929        );
28930    }
28931
28932    #[test]
28933    fn create_table_vector_using_sq8() {
28934        // v6.0.1: `USING SQ8` selects scalar-quantised encoding.
28935        // Case-insensitive on both `USING` and the encoding name.
28936        for sql in [
28937            "CREATE TABLE t (v VECTOR(128) USING SQ8)",
28938            "CREATE TABLE t (v VECTOR(128) using sq8)",
28939        ] {
28940            let s = parse(sql);
28941            let Statement::CreateTable(c) = s else {
28942                panic!()
28943            };
28944            assert_eq!(
28945                c.columns[0].ty,
28946                ColumnTypeName::Vector {
28947                    dim: 128,
28948                    encoding: VecEncoding::Sq8,
28949                },
28950                "{sql}",
28951            );
28952        }
28953    }
28954
28955    #[test]
28956    fn create_table_vector_using_unknown_errors() {
28957        // v7.16.1 — the inline `USING <encoding>` shape on
28958        // CREATE TABLE column defs was withdrawn before
28959        // v7.14.0 in favour of `CREATE INDEX … USING hnsw
28960        // (col vector_<metric>_ops)`; the parser now rejects
28961        // USING at column-list position with a clearer
28962        // "expected ',' or ')'" message. Test asserts the
28963        // current rejection, not the old "unknown vector
28964        // encoding" string.
28965        let err = parse_statement("CREATE TABLE t (v VECTOR(8) USING PQ8)").unwrap_err();
28966        assert!(
28967            err.message.contains("USING")
28968                || err.message.contains("using")
28969                || err.message.contains("')'")
28970                || err.message.contains("','"),
28971            "expected USING/column-list rejection, got: {}",
28972            err.message
28973        );
28974    }
28975
28976    #[test]
28977    fn vector_using_sq8_display_roundtrips() {
28978        // The Display impl must produce text that re-parses to the
28979        // same AST. Guard for the v6.0.1 `USING SQ8` suffix.
28980        let s = parse("CREATE TABLE t (v VECTOR(64) USING SQ8)");
28981        let Statement::CreateTable(c) = s else {
28982            panic!()
28983        };
28984        assert_eq!(c.columns[0].ty.to_string(), "VECTOR(64) USING SQ8");
28985    }
28986
28987    #[test]
28988    fn parser_recognises_placeholders() {
28989        use crate::ast::{Expr, SelectItem, Statement};
28990        // $N in expression position parses as Expr::Placeholder(N).
28991        let s = parse("SELECT $1, $2 + 1 FROM t WHERE x = $3");
28992        let Statement::Select(sel) = s else { panic!() };
28993        assert!(matches!(
28994            sel.items[0],
28995            SelectItem::Expr {
28996                expr: Expr::Placeholder(1),
28997                alias: None
28998            }
28999        ));
29000        // $2 + 1
29001        let SelectItem::Expr {
29002            expr: Expr::Binary { lhs, rhs, .. },
29003            ..
29004        } = &sel.items[1]
29005        else {
29006            panic!()
29007        };
29008        assert!(matches!(**lhs, Expr::Placeholder(2)));
29009        assert!(matches!(**rhs, Expr::Literal(Literal::Integer(1))));
29010        // WHERE x = $3
29011        let Some(Expr::Binary { rhs, .. }) = sel.where_.as_ref() else {
29012            panic!()
29013        };
29014        assert!(matches!(**rhs, Expr::Placeholder(3)));
29015    }
29016
29017    #[test]
29018    fn parser_rejects_dollar_zero() {
29019        // $0 is not valid in PG; the lexer rejects it.
29020        assert!(parse_statement("SELECT $0").is_err());
29021    }
29022
29023    #[test]
29024    fn placeholder_display_roundtrips() {
29025        // The Display impl must produce text that re-lexes to the
29026        // same Placeholder token.
29027        let s = parse("SELECT $42 FROM t");
29028        let printed = s.to_string();
29029        assert!(printed.contains("$42"));
29030        let again = parse(&printed);
29031        assert_eq!(s, again);
29032    }
29033
29034    #[test]
29035    fn alter_index_rebuild_bare() {
29036        use crate::ast::{AlterIndexTarget, Statement};
29037        let s = parse("ALTER INDEX my_idx REBUILD");
29038        let Statement::AlterIndex(a) = s else {
29039            panic!("expected AlterIndex, got {s:?}")
29040        };
29041        assert_eq!(a.name, "my_idx");
29042        assert_eq!(a.target, AlterIndexTarget::Rebuild { encoding: None });
29043    }
29044
29045    #[test]
29046    fn alter_index_rebuild_with_encoding() {
29047        use crate::ast::{AlterIndexTarget, Statement};
29048        for (sql, want) in [
29049            (
29050                "ALTER INDEX my_idx REBUILD WITH (encoding = F32)",
29051                VecEncoding::F32,
29052            ),
29053            (
29054                "ALTER INDEX my_idx REBUILD WITH (encoding = sq8)",
29055                VecEncoding::Sq8,
29056            ),
29057            (
29058                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29059                VecEncoding::F16,
29060            ),
29061        ] {
29062            let s = parse(sql);
29063            let Statement::AlterIndex(a) = s else {
29064                panic!("{sql}: expected AlterIndex")
29065            };
29066            assert_eq!(a.name, "my_idx");
29067            assert_eq!(
29068                a.target,
29069                AlterIndexTarget::Rebuild {
29070                    encoding: Some(want)
29071                },
29072                "{sql}"
29073            );
29074        }
29075    }
29076
29077    #[test]
29078    fn alter_index_rebuild_unknown_encoding_errors() {
29079        let err = parse_statement("ALTER INDEX my_idx REBUILD WITH (encoding = PQ8)").unwrap_err();
29080        assert!(
29081            err.message.contains("unknown vector encoding"),
29082            "got: {}",
29083            err.message
29084        );
29085    }
29086
29087    #[test]
29088    fn alter_index_rebuild_display_roundtrips() {
29089        for (input, want) in [
29090            ("ALTER INDEX my_idx REBUILD", "ALTER INDEX my_idx REBUILD"),
29091            (
29092                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
29093                "ALTER INDEX my_idx REBUILD WITH (encoding = SQ8)",
29094            ),
29095            (
29096                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29097                "ALTER INDEX my_idx REBUILD WITH (encoding = HALF)",
29098            ),
29099        ] {
29100            let s = parse(input);
29101            assert_eq!(s.to_string(), want);
29102        }
29103    }
29104
29105    #[test]
29106    fn create_table_unknown_type_defers_to_engine() {
29107        // v4.9 picked XML as a parse-time "unsupported column
29108        // type" probe. v7.17.0 Phase 1.4 changed the contract:
29109        // an unknown type ident parses as Text + `user_type_ref`
29110        // so CREATE TABLE can resolve user-defined enum / domain
29111        // types — rejection of truly-unknown types moved to the
29112        // engine's catalog lookup. v7.37.5 ζ-A then promoted XML
29113        // to a first-class built-in, so this probe switched to a
29114        // synthetic name nothing in the lexer will ever recognise.
29115        let stmt = parse_statement("CREATE TABLE x (a my_user_type)").unwrap();
29116        let Statement::CreateTable(t) = stmt else {
29117            panic!("expected CreateTable");
29118        };
29119        assert_eq!(t.columns[0].user_type_ref.as_deref(), Some("my_user_type"));
29120    }
29121
29122    #[test]
29123    fn create_table_missing_table_keyword_errors() {
29124        assert!(parse_statement("CREATE x (a INT)").is_err());
29125    }
29126
29127    // v7.37.6-B(sentori Epic 2 P0)— `PARTITION BY RANGE` parent +
29128    // `PARTITION OF parent <bounds>` child parse + Display round-trip.
29129
29130    #[test]
29131    fn parse_create_table_partition_by_range() {
29132        use crate::ast::{PartitionBySpec, PartitionKindAst};
29133        let stmt = parse_statement(
29134            "CREATE TABLE events_partitioned (id BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, \
29135             payload JSONB) PARTITION BY RANGE (ts)",
29136        )
29137        .unwrap();
29138        let Statement::CreateTable(t) = stmt else {
29139            panic!("expected CreateTable");
29140        };
29141        assert!(t.partition_of.is_none(), "parent has no partition_of");
29142        assert_eq!(t.columns.len(), 3);
29143        let by = t.partition_by.as_ref().expect("expected PARTITION BY");
29144        assert_eq!(
29145            by,
29146            &PartitionBySpec {
29147                kind: PartitionKindAst::Range,
29148                key_columns: alloc::vec!["ts".to_string()],
29149            }
29150        );
29151        // Display round-trip preserves the suffix. `quote_ident`
29152        // only adds double quotes when the ident needs escaping, so
29153        // a plain `ts` survives bare here.
29154        assert!(
29155            t.to_string().contains("PARTITION BY RANGE (ts)"),
29156            "Display lost PARTITION BY suffix: {t}"
29157        );
29158    }
29159
29160    #[test]
29161    fn parse_create_table_partition_of_range() {
29162        use crate::ast::{PartitionOfBoundsAst, PartitionOfSpec};
29163        let stmt = parse_statement(
29164            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned \
29165             FOR VALUES FROM ('2026-06-01 00:00:00+00') TO ('2026-07-01 00:00:00+00')",
29166        )
29167        .unwrap();
29168        let Statement::CreateTable(t) = stmt else {
29169            panic!("expected CreateTable");
29170        };
29171        assert!(t.columns.is_empty(), "child inherits columns from parent");
29172        assert!(t.partition_by.is_none());
29173        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29174        assert_eq!(of.parent_name, "events_partitioned");
29175        let PartitionOfSpec { bounds, .. } = of.clone();
29176        match bounds {
29177            PartitionOfBoundsAst::Range { lower, upper } => {
29178                assert!(lower.to_string().contains("2026-06-01"));
29179                assert!(upper.to_string().contains("2026-07-01"));
29180            }
29181            other => panic!("expected Range, got {other:?}"),
29182        }
29183        // Display round-trip emits the FOR VALUES tail. `quote_ident`
29184        // skips quotes when not required, so the parent name appears
29185        // bare here.
29186        let s = t.to_string();
29187        assert!(
29188            s.contains("PARTITION OF events_partitioned"),
29189            "Display lost PARTITION OF: {s}"
29190        );
29191        assert!(s.contains("FOR VALUES FROM"), "Display lost FROM: {s}");
29192        assert!(s.contains(") TO ("), "Display lost TO: {s}");
29193    }
29194
29195    #[test]
29196    fn parse_create_table_partition_of_default() {
29197        use crate::ast::PartitionOfBoundsAst;
29198        let stmt =
29199            parse_statement("CREATE TABLE events_default PARTITION OF events_partitioned DEFAULT")
29200                .unwrap();
29201        let Statement::CreateTable(t) = stmt else {
29202            panic!("expected CreateTable");
29203        };
29204        let of = t.partition_of.as_ref().expect("expected PARTITION OF");
29205        assert_eq!(of.parent_name, "events_partitioned");
29206        assert!(matches!(of.bounds, PartitionOfBoundsAst::Default));
29207        assert!(
29208            t.to_string()
29209                .contains("PARTITION OF events_partitioned DEFAULT"),
29210            "Display lost DEFAULT: {t}"
29211        );
29212    }
29213
29214    #[test]
29215    fn parse_create_table_partition_by_list() {
29216        // v7.37.16 (16.1) — `PARTITION BY LIST (key)` parent + a
29217        // child with `FOR VALUES IN (lit, lit, …)`.
29218        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29219        let parent =
29220            parse_statement("CREATE TABLE events_listed (region TEXT) PARTITION BY LIST (region)")
29221                .unwrap();
29222        let Statement::CreateTable(t) = parent else {
29223            panic!("expected CreateTable");
29224        };
29225        let Some(PartitionBySpec {
29226            kind,
29227            ref key_columns,
29228        }) = t.partition_by
29229        else {
29230            panic!("expected PARTITION BY");
29231        };
29232        assert_eq!(kind, PartitionKindAst::List);
29233        assert_eq!(*key_columns, vec!["region".to_string()]);
29234        assert!(t.to_string().contains("PARTITION BY LIST (region)"));
29235
29236        let child = parse_statement(
29237            "CREATE TABLE events_apac PARTITION OF events_listed \
29238             FOR VALUES IN ('jp', 'kr', 'tw')",
29239        )
29240        .unwrap();
29241        let Statement::CreateTable(c) = child else {
29242            panic!("expected CreateTable");
29243        };
29244        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29245        let PartitionOfBoundsAst::List { values } = &of.bounds else {
29246            panic!("expected List bounds, got {:?}", of.bounds);
29247        };
29248        assert_eq!(values.len(), 3);
29249        let disp = c.to_string();
29250        assert!(disp.contains("FOR VALUES IN ("), "Display lost IN: {disp}");
29251    }
29252
29253    #[test]
29254    fn parse_create_table_partition_by_hash() {
29255        // v7.37.16 (16.2) — `PARTITION BY HASH (key)` parent + a
29256        // child with `FOR VALUES WITH (MODULUS m, REMAINDER r)`.
29257        use crate::ast::{PartitionBySpec, PartitionKindAst, PartitionOfBoundsAst};
29258        let parent =
29259            parse_statement("CREATE TABLE orders_h (id BIGINT) PARTITION BY HASH (id)").unwrap();
29260        let Statement::CreateTable(t) = parent else {
29261            panic!("expected CreateTable");
29262        };
29263        let Some(PartitionBySpec {
29264            kind,
29265            ref key_columns,
29266        }) = t.partition_by
29267        else {
29268            panic!("expected PARTITION BY");
29269        };
29270        assert_eq!(kind, PartitionKindAst::Hash);
29271        assert_eq!(*key_columns, vec!["id".to_string()]);
29272        assert!(t.to_string().contains("PARTITION BY HASH (id)"));
29273
29274        let child = parse_statement(
29275            "CREATE TABLE orders_h_0 PARTITION OF orders_h \
29276             FOR VALUES WITH (MODULUS 4, REMAINDER 0)",
29277        )
29278        .unwrap();
29279        let Statement::CreateTable(c) = child else {
29280            panic!("expected CreateTable");
29281        };
29282        let of = c.partition_of.as_ref().expect("expected PARTITION OF");
29283        let PartitionOfBoundsAst::Hash { modulus, remainder } = of.bounds else {
29284            panic!("expected Hash bounds");
29285        };
29286        assert_eq!(modulus, 4);
29287        assert_eq!(remainder, 0);
29288        let disp = c.to_string();
29289        assert!(
29290            disp.contains("FOR VALUES WITH (MODULUS 4, REMAINDER 0)"),
29291            "Display lost HASH bounds: {disp}"
29292        );
29293
29294        // Validation: REMAINDER ≥ MODULUS is rejected at parse time.
29295        let bad = parse_statement(
29296            "CREATE TABLE orders_h_bad PARTITION OF orders_h \
29297             FOR VALUES WITH (MODULUS 4, REMAINDER 4)",
29298        );
29299        let msg = format!("{}", bad.unwrap_err());
29300        assert!(
29301            msg.contains("REMAINDER") && msg.contains("MODULUS"),
29302            "expected REMAINDER/MODULUS validation error: {msg}"
29303        );
29304    }
29305
29306    #[test]
29307    fn parse_create_table_partition_of_rejects_columns() {
29308        // v7.37.6-B contract: PARTITION OF children inherit columns
29309        // from the parent; an explicit list MUST surface as a parse
29310        // error rather than getting silently ignored.
29311        let err = parse_statement(
29312            "CREATE TABLE events_2026_06 PARTITION OF events_partitioned (id BIGINT) \
29313             FOR VALUES FROM ('a') TO ('b')",
29314        );
29315        assert!(err.is_err(), "expected parse error for explicit columns");
29316        let msg = format!("{}", err.unwrap_err());
29317        assert!(
29318            msg.contains("PARTITION OF") && msg.contains("column"),
29319            "error should mention PARTITION OF + columns: {msg}"
29320        );
29321    }
29322
29323    #[test]
29324    fn insert_single_value() {
29325        let s = parse("INSERT INTO foo VALUES (42)");
29326        let Statement::Insert(i) = s else {
29327            panic!("expected Insert")
29328        };
29329        assert_eq!(i.table, "foo");
29330        assert_eq!(i.rows.len(), 1);
29331        assert_eq!(i.rows[0].len(), 1);
29332        assert!(matches!(i.rows[0][0], Expr::Literal(Literal::Integer(42))));
29333    }
29334
29335    #[test]
29336    fn insert_multi_value_with_mixed_literals() {
29337        let s = parse("INSERT INTO foo VALUES (1, 'hi', 3.14, TRUE, NULL)");
29338        let Statement::Insert(i) = s else { panic!() };
29339        assert_eq!(i.rows.len(), 1);
29340        assert_eq!(i.rows[0].len(), 5);
29341    }
29342
29343    #[test]
29344    fn insert_missing_into_errors() {
29345        assert!(parse_statement("INSERT foo VALUES (1)").is_err());
29346    }
29347
29348    #[test]
29349    fn create_table_round_trip() {
29350        let original =
29351            parse("CREATE TABLE foo (id BIGINT NOT NULL, label TEXT, score FLOAT NOT NULL)");
29352        let text = original.to_string();
29353        let again = parse_statement(&text).expect("re-parse");
29354        assert_eq!(original, again);
29355    }
29356
29357    #[test]
29358    fn insert_round_trip_with_negation_and_string() {
29359        let original = parse("INSERT INTO t VALUES (-1, 'it''s', NULL)");
29360        let text = original.to_string();
29361        let again = parse_statement(&text).expect("re-parse");
29362        assert_eq!(original, again);
29363    }
29364
29365    #[test]
29366    fn unknown_keyword_at_statement_start_errors() {
29367        // v4.4: UPDATE is real SQL now. Use a fabricated keyword so
29368        // the top-level dispatch still has no branch to take.
29369        let err = parse_statement("FROBNICATE foo SET x = 1").unwrap_err();
29370        assert_eq!(err.message, "syntax error at or near \"FROBNICATE\"");
29371    }
29372
29373    // --- v0.8 CREATE INDEX --------------------------------------------------
29374
29375    #[test]
29376    fn create_index_basic() {
29377        let s = parse("CREATE INDEX idx_id ON users (id)");
29378        let Statement::CreateIndex(c) = s else {
29379            panic!("expected CreateIndex")
29380        };
29381        assert_eq!(c.name, "idx_id");
29382        assert_eq!(c.table, "users");
29383        assert_eq!(c.column, "id");
29384    }
29385
29386    #[test]
29387    fn create_index_missing_on_errors() {
29388        assert!(parse_statement("CREATE INDEX foo users (id)").is_err());
29389    }
29390
29391    #[test]
29392    fn create_index_missing_paren_errors() {
29393        assert!(parse_statement("CREATE INDEX foo ON users id").is_err());
29394    }
29395
29396    #[test]
29397    fn create_index_round_trip() {
29398        let original = parse("CREATE INDEX by_name ON users (name)");
29399        let again = parse_statement(&original.to_string()).unwrap();
29400        assert_eq!(original, again);
29401    }
29402
29403    // --- v7.9.29 CREATE UNIQUE INDEX [WHERE pred] (mailrs K1) -------------
29404
29405    #[test]
29406    fn create_unique_index_basic() {
29407        let s = parse("CREATE UNIQUE INDEX uq_x ON t (a)");
29408        let Statement::CreateIndex(c) = s else {
29409            panic!("expected CreateIndex");
29410        };
29411        assert!(c.is_unique);
29412        assert_eq!(c.column, "a");
29413        assert!(c.partial_predicate.is_none());
29414    }
29415
29416    #[test]
29417    fn create_unique_index_partial() {
29418        // mailrs's email_templates "one default per user" shape.
29419        let s = parse(
29420            "CREATE UNIQUE INDEX idx_email_templates_user_default \
29421             ON email_templates (user_address) WHERE is_default = true",
29422        );
29423        let Statement::CreateIndex(c) = s else {
29424            panic!("expected CreateIndex");
29425        };
29426        assert!(c.is_unique);
29427        assert_eq!(c.table, "email_templates");
29428        assert_eq!(c.column, "user_address");
29429        assert!(c.partial_predicate.is_some());
29430    }
29431
29432    #[test]
29433    fn create_unique_index_composite_with_predicate() {
29434        // mailrs's calendar_events instance: composite columns.
29435        let s = parse(
29436            "CREATE UNIQUE INDEX uq_calendar_events_instance \
29437             ON calendar_events (calendar_id, uid, recurrence_id) \
29438             WHERE recurrence_id IS NOT NULL",
29439        );
29440        let Statement::CreateIndex(c) = s else {
29441            panic!("expected CreateIndex");
29442        };
29443        assert!(c.is_unique);
29444        assert_eq!(c.column, "calendar_id");
29445        assert_eq!(
29446            c.extra_columns,
29447            vec!["uid".to_string(), "recurrence_id".to_string()]
29448        );
29449        assert!(c.partial_predicate.is_some());
29450    }
29451
29452    #[test]
29453    fn create_unique_index_using_btree_ok() {
29454        let s = parse("CREATE UNIQUE INDEX uq_x ON t USING btree (a)");
29455        assert!(matches!(s, Statement::CreateIndex(ref c) if c.is_unique));
29456    }
29457
29458    #[test]
29459    fn create_unique_index_using_hnsw_rejected() {
29460        let err =
29461            parse_statement("CREATE UNIQUE INDEX uq_v ON t USING hnsw (embedding)").unwrap_err();
29462        assert!(err.message.contains("UNIQUE"), "{}", err.message);
29463    }
29464
29465    #[test]
29466    fn create_unique_index_round_trip() {
29467        let original = parse(
29468            "CREATE UNIQUE INDEX uq_calendar_events_master \
29469             ON calendar_events (calendar_id, uid) WHERE recurrence_id IS NULL",
29470        );
29471        let again = parse_statement(&original.to_string()).unwrap();
29472        assert_eq!(original, again);
29473    }
29474
29475    #[test]
29476    fn create_unique_without_index_errors() {
29477        let err = parse_statement("CREATE UNIQUE TABLE t (a INT)").unwrap_err();
29478        // v7.39 (round 340, V56) — PG 18.4, verbatim.
29479        assert_eq!(err.message, "syntax error at or near \"TABLE\"");
29480    }
29481
29482    // --- v7.10.4 BYTES / BYTEA column type (Epic 1) ----------------------
29483
29484    #[test]
29485    fn create_table_bytea_column() {
29486        let s = parse("CREATE TABLE t (id INT NOT NULL, payload BYTEA NOT NULL)");
29487        let Statement::CreateTable(c) = s else {
29488            panic!("expected CreateTable");
29489        };
29490        assert_eq!(c.columns.len(), 2);
29491        assert_eq!(c.columns[1].ty, ColumnTypeName::Bytes);
29492        assert!(!c.columns[1].nullable);
29493    }
29494
29495    #[test]
29496    fn create_table_bytes_alias_column() {
29497        let s = parse("CREATE TABLE t (blob BYTES)");
29498        let Statement::CreateTable(c) = s else {
29499            panic!("expected CreateTable");
29500        };
29501        assert_eq!(c.columns[0].ty, ColumnTypeName::Bytes);
29502    }
29503
29504    #[test]
29505    fn bytea_round_trip_display() {
29506        let original = parse("CREATE TABLE t (a BYTEA NOT NULL)");
29507        let again = parse_statement(&original.to_string()).unwrap();
29508        assert_eq!(original, again);
29509    }
29510
29511    // --- v0.9 transactions -------------------------------------------------
29512
29513    #[test]
29514    fn begin_commit_rollback_parse_as_unit_variants() {
29515        let plain = crate::ast::TransactionModes::default();
29516        assert_eq!(parse("BEGIN"), Statement::Begin(plain));
29517        assert_eq!(parse("COMMIT"), Statement::Commit);
29518        // r1066 — PG synonyms pgbench's tpcb script relies on.
29519        assert_eq!(parse("END"), Statement::Commit);
29520        assert_eq!(parse("END TRANSACTION"), Statement::Commit);
29521        assert_eq!(parse("COMMIT WORK"), Statement::Commit);
29522        assert_eq!(parse("ROLLBACK"), Statement::Rollback);
29523        // Trailing semicolons accepted too.
29524        assert_eq!(parse("BEGIN;"), Statement::Begin(plain));
29525        // v7.39 (read01 round 118, B3) — an explicit ISOLATION LEVEL rides the
29526        // statement (with or without the WORK/TRANSACTION noise word).
29527        assert_eq!(
29528            parse("BEGIN ISOLATION LEVEL REPEATABLE READ"),
29529            Statement::Begin(crate::ast::TransactionModes {
29530                isolation: Some(IsolationLevel::RepeatableRead),
29531                read_only: None,
29532            })
29533        );
29534        assert_eq!(
29535            parse("START TRANSACTION ISOLATION LEVEL SERIALIZABLE"),
29536            Statement::Begin(crate::ast::TransactionModes {
29537                isolation: Some(IsolationLevel::Serializable),
29538                read_only: None,
29539            })
29540        );
29541        // v7.39 — this line used to read
29542        //
29543        //     // A non-isolation mode keeps the session default (None).
29544        //     assert_eq!(parse("BEGIN READ ONLY"), Statement::Begin(None));
29545        //
29546        // which pinned the defect rather than catching it: the READ ONLY
29547        // was thrown away, so the statement opened an ordinary read-write
29548        // transaction and every write inside it was accepted. The
29549        // isolation level is still absent here, because this statement
29550        // does not name one — that part was right.
29551        assert_eq!(
29552            parse("BEGIN READ ONLY"),
29553            Statement::Begin(crate::ast::TransactionModes {
29554                isolation: None,
29555                read_only: Some(true),
29556            })
29557        );
29558        assert_eq!(
29559            parse("START TRANSACTION READ WRITE"),
29560            Statement::Begin(crate::ast::TransactionModes {
29561                isolation: None,
29562                read_only: Some(false),
29563            })
29564        );
29565        assert_eq!(
29566            parse("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY"),
29567            Statement::Begin(crate::ast::TransactionModes {
29568                isolation: Some(IsolationLevel::Serializable),
29569                read_only: Some(true),
29570            })
29571        );
29572    }
29573
29574    // --- v1.2: pgvector distance ops + ::vector cast --------------------
29575
29576    #[test]
29577    fn inner_product_binop_parses() {
29578        let s = parse("SELECT v <#> [1.0, 2.0] FROM t");
29579        let Statement::Select(s) = s else { panic!() };
29580        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29581            panic!()
29582        };
29583        assert!(matches!(
29584            expr,
29585            Expr::Binary {
29586                op: BinOp::InnerProduct,
29587                ..
29588            }
29589        ));
29590    }
29591
29592    #[test]
29593    fn cosine_distance_binop_parses() {
29594        let s = parse("SELECT v <=> [1.0, 2.0] FROM t");
29595        let Statement::Select(s) = s else { panic!() };
29596        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29597            panic!()
29598        };
29599        assert!(matches!(
29600            expr,
29601            Expr::Binary {
29602                op: BinOp::CosineDistance,
29603                ..
29604            }
29605        ));
29606    }
29607
29608    #[test]
29609    fn vector_cast_postfix_wraps_string_literal() {
29610        let s = parse("SELECT '[1,2,3]'::vector FROM t");
29611        let Statement::Select(s) = s else { panic!() };
29612        let SelectItem::Expr { expr, .. } = &s.items[0] else {
29613            panic!()
29614        };
29615        assert!(matches!(
29616            expr,
29617            Expr::Cast {
29618                target: CastTarget::Vector,
29619                ..
29620            }
29621        ));
29622    }
29623
29624    #[test]
29625    fn unsupported_cast_target_errors() {
29626        // v7.37.5 ship triage promoted the parser to accept every
29627        // ident as a `CastTarget::Named(canonical)`; the engine
29628        // surfaces the "unsupported cast target" error at eval
29629        // time when `type_name_to_data_type` can't resolve it.
29630        // Parser-side error now requires a NON-ident after `::`
29631        // (e.g. a punctuation token).
29632        let err = parse_statement("SELECT 1::, FROM t").unwrap_err();
29633        assert_eq!(err.message, "syntax error at or near \",\"");
29634    }
29635
29636    #[test]
29637    fn tx_statements_round_trip() {
29638        for q in ["BEGIN", "COMMIT", "ROLLBACK"] {
29639            let original = parse(q);
29640            let again = parse_statement(&original.to_string()).unwrap();
29641            assert_eq!(original, again);
29642        }
29643    }
29644
29645    #[test]
29646    fn interval_text_parsing_units() {
29647        // v7.37.5 β — three-field shape `(months, days, micros)` so
29648        // `'1 day'` and `'24 hours'` no longer collide (PG parity).
29649        // Single unit.
29650        assert_eq!(parse_interval_text("1 day"), Some((0, 1, 0)));
29651        assert_eq!(
29652            parse_interval_text("24 hours"),
29653            Some((0, 0, 86_400_000_000))
29654        );
29655        assert_eq!(parse_interval_text("1 second"), Some((0, 0, 1_000_000)));
29656        assert_eq!(parse_interval_text("1 month"), Some((1, 0, 0)));
29657        assert_eq!(parse_interval_text("2 years"), Some((24, 0, 0)));
29658        assert_eq!(parse_interval_text("1 week"), Some((0, 7, 0)));
29659        // Compound spans accumulate per-dimension.
29660        assert_eq!(parse_interval_text("1 year 6 months"), Some((18, 0, 0)));
29661        assert_eq!(
29662            parse_interval_text("1 day 2 hours"),
29663            Some((0, 1, 7_200_000_000))
29664        );
29665        // Negative numbers carry through per-dimension.
29666        assert_eq!(parse_interval_text("-1 day"), Some((0, -1, 0)));
29667        // Bad shapes return None.
29668        assert_eq!(parse_interval_text(""), None);
29669        assert_eq!(parse_interval_text("garbage"), None);
29670        assert_eq!(parse_interval_text("1 fortnight"), None);
29671        // v7.39 (GUC knife 3) — PG reads a bare number as SECONDS
29672        // (`INTERVAL '1'` = 00:00:01), verified against the oracle.
29673        assert_eq!(parse_interval_text("1"), Some((0, 0, 1_000_000)));
29674        assert_eq!(parse_interval_text("0"), Some((0, 0, 0)));
29675        assert_eq!(parse_interval_text("1.5"), Some((0, 0, 1_500_000)));
29676    }
29677
29678    #[test]
29679    fn interval_literal_roundtrips_via_display() {
29680        let parsed = parse("SELECT INTERVAL '1 day 2 hours'");
29681        let s = parsed.to_string();
29682        // Display preserves the original text verbatim.
29683        assert!(s.contains("INTERVAL '1 day 2 hours'"), "got: {s}");
29684        // And re-parsing yields a structurally equal statement.
29685        let again = parse_statement(&s).unwrap();
29686        assert_eq!(parsed, again);
29687    }
29688
29689    // ── v6.1.2: CREATE / DROP PUBLICATION ────────────────────
29690
29691    #[test]
29692    fn parser_recognises_create_publication_bare() {
29693        let s = parse("CREATE PUBLICATION pub_a");
29694        let Statement::CreatePublication(p) = s else {
29695            panic!("expected CreatePublication, got {s:?}")
29696        };
29697        assert_eq!(p.name, "pub_a");
29698        assert_eq!(p.scope, PublicationScope::AllTables);
29699    }
29700
29701    #[test]
29702    fn parser_recognises_create_publication_for_all_tables() {
29703        let s = parse("CREATE PUBLICATION pub_a FOR ALL TABLES");
29704        let Statement::CreatePublication(p) = s else {
29705            panic!("expected CreatePublication, got {s:?}")
29706        };
29707        assert_eq!(p.name, "pub_a");
29708        assert_eq!(p.scope, PublicationScope::AllTables);
29709    }
29710
29711    #[test]
29712    fn parser_recognises_drop_publication() {
29713        let s = parse("DROP PUBLICATION pub_a");
29714        let Statement::DropPublication { name, .. } = s else {
29715            panic!("expected DropPublication, got {s:?}")
29716        };
29717        assert_eq!(name, "pub_a");
29718    }
29719
29720    #[test]
29721    fn parser_recognises_for_table_list() {
29722        let s = parse("CREATE PUBLICATION pub_a FOR TABLE t1, t2, t3");
29723        let Statement::CreatePublication(p) = s else {
29724            panic!("expected CreatePublication, got {s:?}")
29725        };
29726        assert_eq!(p.name, "pub_a");
29727        let PublicationScope::ForTables(ts) = p.scope else {
29728            panic!("expected ForTables scope")
29729        };
29730        assert_eq!(ts, alloc::vec!["t1", "t2", "t3"]);
29731    }
29732
29733    #[test]
29734    fn parser_rejects_bare_for_tables_and_takes_in_schema() {
29735        // v7.39 (round 754, F31-B5) — PG18-measured: the bare plural
29736        // is rejected (`invalid publication object list`; the old
29737        // test pinned an unverifiable "PG 19 accepts both" claim);
29738        // TABLES pairs with IN SCHEMA.
29739        let err = parse_statement("CREATE PUBLICATION pub_a FOR TABLES t1, t2")
29740            .expect_err("bare FOR TABLES must reject");
29741        assert!(
29742            alloc::format!("{err}").contains("invalid publication object list"),
29743            "got: {err}"
29744        );
29745        let s = parse("CREATE PUBLICATION pub_a FOR TABLES IN SCHEMA public");
29746        let Statement::CreatePublication(p) = s else {
29747            panic!("expected CreatePublication, got {s:?}")
29748        };
29749        let PublicationScope::TablesInSchema(schema) = p.scope else {
29750            panic!("expected TablesInSchema")
29751        };
29752        assert_eq!(schema, "public");
29753    }
29754
29755    #[test]
29756    fn parser_recognises_for_all_tables_except_list() {
29757        let s = parse("CREATE PUBLICATION p FOR ALL TABLES EXCEPT t1, t2");
29758        let Statement::CreatePublication(p) = s else {
29759            panic!()
29760        };
29761        let PublicationScope::AllTablesExcept(ts) = p.scope else {
29762            panic!("expected AllTablesExcept")
29763        };
29764        assert_eq!(ts, alloc::vec!["t1", "t2"]);
29765    }
29766
29767    #[test]
29768    fn parser_rejects_for_table_with_empty_list() {
29769        // `FOR TABLE` with nothing after is a parse error.
29770        let err = parse_statement("CREATE PUBLICATION p FOR TABLE")
29771            .expect_err("must error on empty list");
29772        // No specific message asserted — the call falls through to
29773        // expect_ident_like which yields "expected identifier, got …".
29774        assert!(!err.message.is_empty());
29775    }
29776
29777    #[test]
29778    fn parser_recognises_show_publications() {
29779        // v6.1.3 — SHOW PUBLICATIONS lands here. PUBLICATIONS is a
29780        // bare ident in this position, NOT a reserved keyword.
29781        let s = parse("SHOW PUBLICATIONS");
29782        assert!(matches!(s, Statement::ShowPublications));
29783    }
29784
29785    // ── v6.1.4: CREATE / DROP SUBSCRIPTION + SHOW SUBSCRIPTIONS ─
29786
29787    #[test]
29788    fn parser_recognises_create_subscription_single_publication() {
29789        let s = parse(
29790            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=127.0.0.1 port=20002' PUBLICATION pub_a",
29791        );
29792        let Statement::CreateSubscription(c) = s else {
29793            panic!("expected CreateSubscription, got {s:?}")
29794        };
29795        assert_eq!(c.name, "sub_a");
29796        assert_eq!(c.conn_str, "host=127.0.0.1 port=20002");
29797        assert_eq!(c.publications, alloc::vec!["pub_a"]);
29798    }
29799
29800    #[test]
29801    fn parser_recognises_create_subscription_multi_publication() {
29802        let s = parse("CREATE SUBSCRIPTION sub_a CONNECTION 'host=h' PUBLICATION p1, p2, p3");
29803        let Statement::CreateSubscription(c) = s else {
29804            panic!()
29805        };
29806        assert_eq!(c.publications, alloc::vec!["p1", "p2", "p3"]);
29807    }
29808
29809    #[test]
29810    fn parser_rejects_create_subscription_missing_connection() {
29811        let err = parse_statement("CREATE SUBSCRIPTION s PUBLICATION p")
29812            .expect_err("must error on missing CONNECTION");
29813        assert_eq!(err.message, "syntax error at or near \"PUBLICATION\"");
29814    }
29815
29816    #[test]
29817    fn parser_rejects_create_subscription_missing_publication() {
29818        let err = parse_statement("CREATE SUBSCRIPTION s CONNECTION 'host=x'")
29819            .expect_err("must error on missing PUBLICATION");
29820        assert_eq!(err.message, "syntax error at end of input");
29821    }
29822
29823    #[test]
29824    fn parser_recognises_drop_subscription() {
29825        let s = parse("DROP SUBSCRIPTION sub_a");
29826        let Statement::DropSubscription { name, .. } = s else {
29827            panic!("expected DropSubscription, got {s:?}")
29828        };
29829        assert_eq!(name, "sub_a");
29830    }
29831
29832    #[test]
29833    fn parser_recognises_show_subscriptions() {
29834        let s = parse("SHOW SUBSCRIPTIONS");
29835        assert!(matches!(s, Statement::ShowSubscriptions));
29836    }
29837
29838    #[test]
29839    fn parser_recognises_wait_for_wal_position_no_timeout() {
29840        let s = parse("WAIT FOR WAL POSITION 12345");
29841        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29842            panic!("expected WaitForWalPosition, got {s:?}")
29843        };
29844        assert_eq!(pos, 12345);
29845        assert!(timeout_ms.is_none());
29846    }
29847
29848    #[test]
29849    fn parser_recognises_wait_for_wal_position_with_timeout() {
29850        let s = parse("WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000");
29851        let Statement::WaitForWalPosition { pos, timeout_ms } = s else {
29852            panic!()
29853        };
29854        assert_eq!(pos, 67890);
29855        assert_eq!(timeout_ms, Some(5000));
29856    }
29857
29858    #[test]
29859    fn parser_rejects_wait_with_negative_position() {
29860        // The lexer treats `-` as a token; `expect_u64_literal`
29861        // only sees the Integer that follows, so the negative
29862        // arrives as a unary-minus expression at higher levels.
29863        // Bare `WAIT FOR WAL POSITION -1` thus surfaces as a
29864        // parse error one way or another.
29865        let err = parse_statement("WAIT FOR WAL POSITION -1").unwrap_err();
29866        assert!(!err.message.is_empty());
29867    }
29868
29869    #[test]
29870    fn parser_recognises_bare_analyze() {
29871        let s = parse("ANALYZE");
29872        assert!(matches!(s, Statement::Analyze(None)));
29873    }
29874
29875    #[test]
29876    fn parser_recognises_analyze_with_table() {
29877        let s = parse("ANALYZE users");
29878        let Statement::Analyze(Some(name)) = s else {
29879            panic!("expected Analyze, got {s:?}")
29880        };
29881        assert_eq!(name, "users");
29882    }
29883
29884    #[test]
29885    fn parser_recognises_analyze_with_quoted_table() {
29886        let s = parse("ANALYZE \"Mixed Case\"");
29887        let Statement::Analyze(Some(name)) = s else {
29888            panic!()
29889        };
29890        assert_eq!(name, "Mixed Case");
29891    }
29892
29893    #[test]
29894    fn parser_rejects_analyze_with_garbage_token() {
29895        let err = parse_statement("ANALYZE 42").expect_err("must error");
29896        assert!(!err.message.is_empty());
29897    }
29898
29899    #[test]
29900    fn analyze_display_roundtrips() {
29901        for sql in ["ANALYZE", "ANALYZE users"] {
29902            let s = parse(sql);
29903            let printed = s.to_string();
29904            let again = parse_statement(&printed)
29905                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29906            assert_eq!(s, again);
29907        }
29908    }
29909
29910    #[test]
29911    fn wait_for_display_roundtrips() {
29912        for sql in [
29913            "WAIT FOR WAL POSITION 12345",
29914            "WAIT FOR WAL POSITION 67890 WITH TIMEOUT 5000",
29915        ] {
29916            let s = parse(sql);
29917            let printed = s.to_string();
29918            let again = parse_statement(&printed)
29919                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29920            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29921        }
29922    }
29923
29924    #[test]
29925    fn subscription_ddl_display_roundtrips() {
29926        for sql in [
29927            "CREATE SUBSCRIPTION sub_a CONNECTION 'host=h port=20002' PUBLICATION pub_a",
29928            "CREATE SUBSCRIPTION sub_b CONNECTION 'host=h' PUBLICATION p1, p2",
29929            "DROP SUBSCRIPTION sub_a",
29930            "SHOW SUBSCRIPTIONS",
29931        ] {
29932            let s = parse(sql);
29933            let printed = s.to_string();
29934            let again = parse_statement(&printed)
29935                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29936            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29937        }
29938    }
29939
29940    #[test]
29941    fn parser_drop_dispatches_user_vs_publication() {
29942        // Pre-v6.1.2 DROP USER took the bare-ident path; v6.1.2
29943        // tokenises DROP. Both targets must still parse.
29944        let s = parse("DROP USER 'alice'");
29945        let Statement::DropUser { name, .. } = s else {
29946            panic!("expected DropUser, got {s:?}")
29947        };
29948        assert_eq!(name, "alice");
29949        // And DROP PUBLICATION lands the new variant.
29950        let s = parse("DROP PUBLICATION p1");
29951        assert!(matches!(s, Statement::DropPublication { .. }));
29952    }
29953
29954    #[test]
29955    fn publication_ddl_display_roundtrips() {
29956        // Every CREATE PUBLICATION variant must Display → parse →
29957        // same AST. v6.1.3 covers all three scope shapes.
29958        for sql in [
29959            "CREATE PUBLICATION pub_a",
29960            "CREATE PUBLICATION pub_a FOR ALL TABLES",
29961            "CREATE PUBLICATION pub_a FOR TABLE t1, t2",
29962            "CREATE PUBLICATION pub_a FOR ALL TABLES EXCEPT t1",
29963            "DROP PUBLICATION pub_a",
29964            "SHOW PUBLICATIONS",
29965        ] {
29966            let s = parse(sql);
29967            let printed = s.to_string();
29968            let again = parse_statement(&printed)
29969                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
29970            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
29971        }
29972    }
29973
29974    // --- v7.12.4: CREATE FUNCTION + CREATE TRIGGER + PL/pgSQL ---
29975
29976    #[test]
29977    fn create_function_returns_trigger_plpgsql_minimal() {
29978        let sql = "CREATE FUNCTION noop() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END; $$";
29979        let s = parse(sql);
29980        let Statement::CreateFunction(f) = s else {
29981            panic!("expected CreateFunction");
29982        };
29983        assert_eq!(f.name, "noop");
29984        assert!(!f.or_replace);
29985        assert!(f.args.is_empty());
29986        assert!(matches!(f.returns, FunctionReturn::Trigger));
29987        assert_eq!(f.language, "plpgsql");
29988        let FunctionBody::PlPgSql(block) = f.body else {
29989            panic!("expected PlPgSql body");
29990        };
29991        assert_eq!(block.statements.len(), 1);
29992        assert!(matches!(
29993            block.statements[0],
29994            PlPgSqlStmt::Return(ReturnTarget::New)
29995        ));
29996    }
29997
29998    #[test]
29999    fn create_function_or_replace_with_assignment() {
30000        // mailrs-shape trigger function: NEW.col := to_tsvector(...);
30001        // RETURN NEW.
30002        let sql = "CREATE OR REPLACE FUNCTION update_sv() RETURNS TRIGGER LANGUAGE plpgsql AS $$
30003BEGIN
30004  NEW.search_vector := to_tsvector('english', NEW.subject);
30005  RETURN NEW;
30006END;
30007$$";
30008        let s = parse(sql);
30009        let Statement::CreateFunction(f) = s else {
30010            panic!("expected CreateFunction");
30011        };
30012        assert!(f.or_replace);
30013        let FunctionBody::PlPgSql(block) = &f.body else {
30014            panic!("expected PlPgSql body");
30015        };
30016        assert_eq!(block.statements.len(), 2);
30017        // First statement: NEW.search_vector := to_tsvector(...)
30018        let PlPgSqlStmt::Assign { target, .. } = &block.statements[0] else {
30019            panic!("expected Assign as first stmt");
30020        };
30021        match target {
30022            AssignTarget::NewColumn(c) => assert_eq!(c, "search_vector"),
30023            other => panic!("expected NEW.col, got {other:?}"),
30024        }
30025        // Second statement: RETURN NEW
30026        assert!(matches!(
30027            block.statements[1],
30028            PlPgSqlStmt::Return(ReturnTarget::New)
30029        ));
30030    }
30031
30032    #[test]
30033    fn create_trigger_after_insert_or_update() {
30034        let sql = "CREATE TRIGGER tg AFTER INSERT OR UPDATE ON messages FOR EACH ROW EXECUTE FUNCTION update_sv()";
30035        let s = parse(sql);
30036        let Statement::CreateTrigger(t) = s else {
30037            panic!("expected CreateTrigger");
30038        };
30039        assert_eq!(t.name, "tg");
30040        assert_eq!(t.table, "messages");
30041        assert_eq!(t.timing, TriggerTiming::After);
30042        assert_eq!(t.events, vec![TriggerEvent::Insert, TriggerEvent::Update]);
30043        assert_eq!(t.for_each, TriggerForEach::Row);
30044        assert_eq!(t.function, "update_sv");
30045    }
30046
30047    #[test]
30048    fn create_trigger_before_delete_execute_procedure_alias() {
30049        // PG also accepts the legacy `EXECUTE PROCEDURE` spelling.
30050        let sql =
30051            "CREATE TRIGGER guard BEFORE DELETE ON t FOR EACH ROW EXECUTE PROCEDURE block_delete()";
30052        let s = parse(sql);
30053        let Statement::CreateTrigger(t) = s else {
30054            panic!("expected CreateTrigger");
30055        };
30056        assert_eq!(t.timing, TriggerTiming::Before);
30057        assert_eq!(t.events, vec![TriggerEvent::Delete]);
30058    }
30059
30060    #[test]
30061    fn drop_trigger_if_exists_round_trips() {
30062        // No parser support for DROP TRIGGER yet — added in v7.12.5
30063        // alongside the broader DROP …{IF EXISTS} cleanup. The
30064        // AST + Display impls are in place so we round-trip via
30065        // construction:
30066        let s = Statement::DropTrigger {
30067            name: "tg".into(),
30068            table: "messages".into(),
30069            if_exists: true,
30070        };
30071        assert_eq!(s.to_string(), "DROP TRIGGER IF EXISTS tg ON messages");
30072    }
30073
30074    #[test]
30075    fn trigger_ddl_display_roundtrips_through_parser() {
30076        // CREATE TRIGGER + its referenced CREATE FUNCTION must
30077        // Display → parse → same AST (modulo PL/pgSQL body
30078        // formatting which is parser-canonicalised).
30079        for sql in [
30080            "CREATE TRIGGER tg AFTER INSERT ON t FOR EACH ROW EXECUTE FUNCTION f()",
30081            "CREATE TRIGGER tg2 BEFORE UPDATE OR DELETE ON t FOR EACH ROW EXECUTE FUNCTION g()",
30082        ] {
30083            let s = parse(sql);
30084            let printed = s.to_string();
30085            let again = parse_statement(&printed)
30086                .unwrap_or_else(|e| panic!("re-parse failed for {printed:?}: {e}"));
30087            assert_eq!(s, again, "round-trip mismatch for {sql:?}");
30088        }
30089    }
30090}